mirror of
https://github.com/actions/cache.git
synced 2024-12-01 11:35:20 +01:00
Merge remote-tracking branch 'origin/700-actionscache-granular-cache-control' into kotewar/readme-updates-for-granular-control
This commit is contained in:
commit
0cc9c1d4e8
31 changed files with 126818 additions and 4302 deletions
1
.github/workflows/check-dist.yml
vendored
1
.github/workflows/check-dist.yml
vendored
|
@ -27,6 +27,7 @@ jobs:
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
node-version: 16.x
|
||||||
|
cache: npm
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Rebuild the dist/ directory
|
- name: Rebuild the dist/ directory
|
||||||
|
|
12
.github/workflows/workflow.yml
vendored
12
.github/workflows/workflow.yml
vendored
|
@ -25,17 +25,7 @@ jobs:
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
node-version: 16.x
|
||||||
- name: Determine npm cache directory
|
cache: npm
|
||||||
id: npm-cache
|
|
||||||
run: |
|
|
||||||
echo "::set-output name=dir::$(npm config get cache)"
|
|
||||||
- name: Restore npm cache
|
|
||||||
uses: actions/cache@v3
|
|
||||||
with:
|
|
||||||
path: ${{ steps.npm-cache.outputs.dir }}
|
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-node-
|
|
||||||
- run: npm ci
|
- run: npm ci
|
||||||
- name: Prettier Format Check
|
- name: Prettier Format Check
|
||||||
run: npm run format-check
|
run: npm run format-check
|
||||||
|
|
|
@ -40,3 +40,9 @@
|
||||||
### 3.0.11
|
### 3.0.11
|
||||||
- Update toolkit version to 3.0.5 to include `@actions/core@^1.10.0`
|
- Update toolkit version to 3.0.5 to include `@actions/core@^1.10.0`
|
||||||
- Update `@actions/cache` to use updated `saveState` and `setOutput` functions from `@actions/core@^1.10.0`
|
- Update `@actions/cache` to use updated `saveState` and `setOutput` functions from `@actions/core@^1.10.0`
|
||||||
|
|
||||||
|
### 3.1.0-beta.1
|
||||||
|
- Update `@actions/cache` on windows to use gnu tar and zstd by default and fallback to bsdtar and zstd if gnu tar is not available. ([issue](https://github.com/actions/cache/issues/984))
|
||||||
|
|
||||||
|
### 3.1.0-beta.2
|
||||||
|
- Added support for fallback to gzip to restore old caches on windows.
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as cache from "@actions/cache";
|
import * as cache from "@actions/cache";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
import { Events, Outputs, RefKey, State } from "../src/constants";
|
import { Events, RefKey } from "../src/constants";
|
||||||
import * as actionUtils from "../src/utils/actionUtils";
|
import * as actionUtils from "../src/utils/actionUtils";
|
||||||
import * as testUtils from "../src/utils/testUtils";
|
import * as testUtils from "../src/utils/testUtils";
|
||||||
|
|
||||||
|
@ -79,83 +79,6 @@ test("isExactKeyMatch with same key and different casing returns true", () => {
|
||||||
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(true);
|
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("setOutputAndState with undefined entry to set cache-hit output", () => {
|
|
||||||
const key = "linux-rust";
|
|
||||||
const cacheKey = undefined;
|
|
||||||
|
|
||||||
const setOutputMock = jest.spyOn(core, "setOutput");
|
|
||||||
const saveStateMock = jest.spyOn(core, "saveState");
|
|
||||||
|
|
||||||
actionUtils.setOutputAndState(key, cacheKey);
|
|
||||||
|
|
||||||
expect(setOutputMock).toHaveBeenCalledWith(Outputs.CacheHit, "false");
|
|
||||||
expect(setOutputMock).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
expect(saveStateMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("setOutputAndState with exact match to set cache-hit output and state", () => {
|
|
||||||
const key = "linux-rust";
|
|
||||||
const cacheKey = "linux-rust";
|
|
||||||
|
|
||||||
const setOutputMock = jest.spyOn(core, "setOutput");
|
|
||||||
const saveStateMock = jest.spyOn(core, "saveState");
|
|
||||||
|
|
||||||
actionUtils.setOutputAndState(key, cacheKey);
|
|
||||||
|
|
||||||
expect(setOutputMock).toHaveBeenCalledWith(Outputs.CacheHit, "true");
|
|
||||||
expect(setOutputMock).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
expect(saveStateMock).toHaveBeenCalledWith(State.CacheMatchedKey, cacheKey);
|
|
||||||
expect(saveStateMock).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("setOutputAndState with no exact match to set cache-hit output and state", () => {
|
|
||||||
const key = "linux-rust";
|
|
||||||
const cacheKey = "linux-rust-bb828da54c148048dd17899ba9fda624811cfb43";
|
|
||||||
|
|
||||||
const setOutputMock = jest.spyOn(core, "setOutput");
|
|
||||||
const saveStateMock = jest.spyOn(core, "saveState");
|
|
||||||
|
|
||||||
actionUtils.setOutputAndState(key, cacheKey);
|
|
||||||
|
|
||||||
expect(setOutputMock).toHaveBeenCalledWith(Outputs.CacheHit, "false");
|
|
||||||
expect(setOutputMock).toHaveBeenCalledTimes(1);
|
|
||||||
|
|
||||||
expect(saveStateMock).toHaveBeenCalledWith(State.CacheMatchedKey, cacheKey);
|
|
||||||
expect(saveStateMock).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getCacheState with no state returns undefined", () => {
|
|
||||||
const getStateMock = jest.spyOn(core, "getState");
|
|
||||||
getStateMock.mockImplementation(() => {
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
|
|
||||||
const state = actionUtils.getCacheState();
|
|
||||||
|
|
||||||
expect(state).toBe(undefined);
|
|
||||||
|
|
||||||
expect(getStateMock).toHaveBeenCalledWith(State.CacheMatchedKey);
|
|
||||||
expect(getStateMock).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getCacheState with valid state", () => {
|
|
||||||
const cacheKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
|
||||||
|
|
||||||
const getStateMock = jest.spyOn(core, "getState");
|
|
||||||
getStateMock.mockImplementation(() => {
|
|
||||||
return cacheKey;
|
|
||||||
});
|
|
||||||
|
|
||||||
const state = actionUtils.getCacheState();
|
|
||||||
|
|
||||||
expect(state).toEqual(cacheKey);
|
|
||||||
|
|
||||||
expect(getStateMock).toHaveBeenCalledWith(State.CacheMatchedKey);
|
|
||||||
expect(getStateMock).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("logWarning logs a message with a warning prefix", () => {
|
test("logWarning logs a message with a warning prefix", () => {
|
||||||
const message = "A warning occurred.";
|
const message = "A warning occurred.";
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import * as cache from "@actions/cache";
|
import * as cache from "@actions/cache";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
import { Events, Inputs, RefKey } from "../src/constants";
|
import { Events, RefKey } from "../src/constants";
|
||||||
import run from "../src/restore";
|
import run from "../src/restore";
|
||||||
import * as actionUtils from "../src/utils/actionUtils";
|
import * as actionUtils from "../src/utils/actionUtils";
|
||||||
import * as testUtils from "../src/utils/testUtils";
|
import * as testUtils from "../src/utils/testUtils";
|
||||||
|
@ -45,158 +45,6 @@ afterEach(() => {
|
||||||
delete process.env[RefKey];
|
delete process.env[RefKey];
|
||||||
});
|
});
|
||||||
|
|
||||||
test("restore with invalid event outputs warning", async () => {
|
|
||||||
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
|
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
|
||||||
const invalidEvent = "commit_comment";
|
|
||||||
process.env[Events.Key] = invalidEvent;
|
|
||||||
delete process.env[RefKey];
|
|
||||||
await run();
|
|
||||||
expect(logWarningMock).toHaveBeenCalledWith(
|
|
||||||
`Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
|
|
||||||
);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore without AC available should no-op", async () => {
|
|
||||||
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
|
|
||||||
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
|
|
||||||
() => false
|
|
||||||
);
|
|
||||||
|
|
||||||
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
|
||||||
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore on GHES without AC available should no-op", async () => {
|
|
||||||
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
|
|
||||||
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
|
|
||||||
() => false
|
|
||||||
);
|
|
||||||
|
|
||||||
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
|
||||||
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore on GHES with AC available ", async () => {
|
|
||||||
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
|
|
||||||
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 stateMock = jest.spyOn(core, "saveState");
|
|
||||||
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
|
|
||||||
const restoreCacheMock = jest
|
|
||||||
.spyOn(cache, "restoreCache")
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return Promise.resolve(key);
|
|
||||||
});
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
|
||||||
|
|
||||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledWith(true);
|
|
||||||
|
|
||||||
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore with no path should fail", async () => {
|
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
|
||||||
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
|
||||||
await run();
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
|
||||||
// this input isn't necessary for restore b/c tarball contains entries relative to workspace
|
|
||||||
expect(failedMock).not.toHaveBeenCalledWith(
|
|
||||||
"Input required and not supplied: path"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore with no key", async () => {
|
|
||||||
testUtils.setInput(Inputs.Path, "node_modules");
|
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
|
||||||
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
|
||||||
await run();
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
|
||||||
expect(failedMock).toHaveBeenCalledWith(
|
|
||||||
"Input required and not supplied: key"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore with too many keys should fail", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "node-test";
|
|
||||||
const restoreKeys = [...Array(20).keys()].map(x => x.toString());
|
|
||||||
testUtils.setInputs({
|
|
||||||
path: path,
|
|
||||||
key,
|
|
||||||
restoreKeys
|
|
||||||
});
|
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
|
||||||
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
|
||||||
await run();
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, restoreKeys);
|
|
||||||
expect(failedMock).toHaveBeenCalledWith(
|
|
||||||
`Key Validation Error: Keys are limited to a maximum of 10.`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore with large key should fail", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "foo".repeat(512); // Over the 512 character limit
|
|
||||||
testUtils.setInputs({
|
|
||||||
path: path,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
|
||||||
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
|
||||||
await run();
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
|
||||||
expect(failedMock).toHaveBeenCalledWith(
|
|
||||||
`Key Validation Error: ${key} cannot be larger than 512 characters.`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore with invalid key should fail", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "comma,comma";
|
|
||||||
testUtils.setInputs({
|
|
||||||
path: path,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
|
||||||
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
|
||||||
await run();
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
|
||||||
expect(failedMock).toHaveBeenCalledWith(
|
|
||||||
`Key Validation Error: ${key} cannot contain commas.`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("restore with no cache found", async () => {
|
test("restore with no cache found", async () => {
|
||||||
const path = "node_modules";
|
const path = "node_modules";
|
||||||
const key = "node-test";
|
const key = "node-test";
|
||||||
|
@ -270,7 +118,7 @@ test("restore with cache found for key", async () => {
|
||||||
const infoMock = jest.spyOn(core, "info");
|
const infoMock = jest.spyOn(core, "info");
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
const stateMock = jest.spyOn(core, "saveState");
|
const stateMock = jest.spyOn(core, "saveState");
|
||||||
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
|
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
|
||||||
const restoreCacheMock = jest
|
const restoreCacheMock = jest
|
||||||
.spyOn(cache, "restoreCache")
|
.spyOn(cache, "restoreCache")
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
|
@ -284,7 +132,7 @@ test("restore with cache found for key", async () => {
|
||||||
|
|
||||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledWith(true);
|
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true");
|
||||||
|
|
||||||
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
|
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
@ -303,7 +151,7 @@ test("restore with cache found for restore key", async () => {
|
||||||
const infoMock = jest.spyOn(core, "info");
|
const infoMock = jest.spyOn(core, "info");
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
const stateMock = jest.spyOn(core, "saveState");
|
const stateMock = jest.spyOn(core, "saveState");
|
||||||
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
|
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
|
||||||
const restoreCacheMock = jest
|
const restoreCacheMock = jest
|
||||||
.spyOn(cache, "restoreCache")
|
.spyOn(cache, "restoreCache")
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
|
@ -317,120 +165,9 @@ test("restore with cache found for restore key", async () => {
|
||||||
|
|
||||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
|
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
|
||||||
|
|
||||||
expect(infoMock).toHaveBeenCalledWith(
|
expect(infoMock).toHaveBeenCalledWith(
|
||||||
`Cache restored from key: ${restoreKey}`
|
`Cache restored from key: ${restoreKey}`
|
||||||
);
|
);
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("restore with enabling save on any failure feature", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "node-test";
|
|
||||||
const restoreKey = "node-";
|
|
||||||
testUtils.setInputs({
|
|
||||||
path: path,
|
|
||||||
key,
|
|
||||||
restoreKeys: [restoreKey],
|
|
||||||
saveOnAnyFailure: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const debugMock = jest.spyOn(core, "debug");
|
|
||||||
const infoMock = jest.spyOn(core, "info");
|
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
|
||||||
const stateMock = jest.spyOn(core, "saveState");
|
|
||||||
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
|
|
||||||
const restoreCacheMock = jest
|
|
||||||
.spyOn(cache, "restoreCache")
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return Promise.resolve(restoreKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, [restoreKey]);
|
|
||||||
|
|
||||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
|
|
||||||
|
|
||||||
expect(debugMock).toHaveBeenCalledWith(
|
|
||||||
`Exporting environment variable SAVE_CACHE_ON_ANY_FAILURE`
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(infoMock).toHaveBeenCalledWith(
|
|
||||||
`Input Variable SAVE_CACHE_ON_ANY_FAILURE is set to true, the cache will be saved despite of any failure in the build.`
|
|
||||||
);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Fail restore when fail on cache miss is enabled and primary key 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(actionUtils, "setCacheHitOutput");
|
|
||||||
const restoreCacheMock = jest
|
|
||||||
.spyOn(cache, "restoreCache")
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return Promise.resolve(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, [restoreKey]);
|
|
||||||
|
|
||||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(0);
|
|
||||||
|
|
||||||
expect(failedMock).toHaveBeenCalledWith(
|
|
||||||
`Cache with the given input key ${key} is not found, hence exiting the workflow as the fail-on-cache-miss requirement is not met.`
|
|
||||||
);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("Fail 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 failedMock = jest.spyOn(core, "setFailed");
|
|
||||||
const stateMock = jest.spyOn(core, "saveState");
|
|
||||||
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
|
|
||||||
const restoreCacheMock = jest
|
|
||||||
.spyOn(cache, "restoreCache")
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return Promise.resolve(restoreKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, [restoreKey]);
|
|
||||||
|
|
||||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
|
|
||||||
|
|
||||||
expect(failedMock).toHaveBeenCalledWith(
|
|
||||||
`Restored cache key doesn't match the given input key ${key}, hence exiting the workflow as the fail-on-cache-miss requirement is not met.`
|
|
||||||
);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
326
__tests__/restoreImpl.test.ts
Normal file
326
__tests__/restoreImpl.test.ts
Normal file
|
@ -0,0 +1,326 @@
|
||||||
|
import * as cache from "@actions/cache";
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
|
import { Events, Inputs, RefKey } from "../src/constants";
|
||||||
|
import run from "../src/restoreImpl";
|
||||||
|
import { StateProvider } from "../src/stateProvider";
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
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("restore with invalid event outputs warning", async () => {
|
||||||
|
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
|
||||||
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
const invalidEvent = "commit_comment";
|
||||||
|
process.env[Events.Key] = invalidEvent;
|
||||||
|
delete process.env[RefKey];
|
||||||
|
await run(new StateProvider());
|
||||||
|
expect(logWarningMock).toHaveBeenCalledWith(
|
||||||
|
`Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
|
||||||
|
);
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("restore without AC available should no-op", async () => {
|
||||||
|
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => false);
|
||||||
|
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
|
||||||
|
() => false
|
||||||
|
);
|
||||||
|
|
||||||
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
||||||
|
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
|
||||||
|
|
||||||
|
await run(new StateProvider());
|
||||||
|
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
||||||
|
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("restore on GHES without AC available should no-op", async () => {
|
||||||
|
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
|
||||||
|
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
|
||||||
|
() => false
|
||||||
|
);
|
||||||
|
|
||||||
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
||||||
|
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
|
||||||
|
|
||||||
|
await run(new StateProvider());
|
||||||
|
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
||||||
|
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("restore on GHES with AC available ", async () => {
|
||||||
|
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
|
||||||
|
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 stateMock = jest.spyOn(core, "saveState");
|
||||||
|
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
|
||||||
|
const restoreCacheMock = jest
|
||||||
|
.spyOn(cache, "restoreCache")
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return Promise.resolve(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
await run(new StateProvider());
|
||||||
|
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
||||||
|
|
||||||
|
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||||
|
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true");
|
||||||
|
|
||||||
|
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("restore with no path should fail", async () => {
|
||||||
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
||||||
|
await run(new StateProvider());
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
||||||
|
// this input isn't necessary for restore b/c tarball contains entries relative to workspace
|
||||||
|
expect(failedMock).not.toHaveBeenCalledWith(
|
||||||
|
"Input required and not supplied: path"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("restore with no key", async () => {
|
||||||
|
testUtils.setInput(Inputs.Path, "node_modules");
|
||||||
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
||||||
|
await run(new StateProvider());
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
||||||
|
expect(failedMock).toHaveBeenCalledWith(
|
||||||
|
"Input required and not supplied: key"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("restore with too many keys should fail", async () => {
|
||||||
|
const path = "node_modules";
|
||||||
|
const key = "node-test";
|
||||||
|
const restoreKeys = [...Array(20).keys()].map(x => x.toString());
|
||||||
|
testUtils.setInputs({
|
||||||
|
path: path,
|
||||||
|
key,
|
||||||
|
restoreKeys
|
||||||
|
});
|
||||||
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
||||||
|
await run(new StateProvider());
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, restoreKeys);
|
||||||
|
expect(failedMock).toHaveBeenCalledWith(
|
||||||
|
`Key Validation Error: Keys are limited to a maximum of 10.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("restore with large key should fail", async () => {
|
||||||
|
const path = "node_modules";
|
||||||
|
const key = "foo".repeat(512); // Over the 512 character limit
|
||||||
|
testUtils.setInputs({
|
||||||
|
path: path,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
||||||
|
await run(new StateProvider());
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
||||||
|
expect(failedMock).toHaveBeenCalledWith(
|
||||||
|
`Key Validation Error: ${key} cannot be larger than 512 characters.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("restore with invalid key should fail", async () => {
|
||||||
|
const path = "node_modules";
|
||||||
|
const key = "comma,comma";
|
||||||
|
testUtils.setInputs({
|
||||||
|
path: path,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
||||||
|
await run(new StateProvider());
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
||||||
|
expect(failedMock).toHaveBeenCalledWith(
|
||||||
|
`Key Validation Error: ${key} cannot contain commas.`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("restore 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 stateMock = jest.spyOn(core, "saveState");
|
||||||
|
const restoreCacheMock = jest
|
||||||
|
.spyOn(cache, "restoreCache")
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
await run(new StateProvider());
|
||||||
|
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
||||||
|
|
||||||
|
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
expect(infoMock).toHaveBeenCalledWith(
|
||||||
|
`Cache not found for input keys: ${key}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("restore 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]
|
||||||
|
});
|
||||||
|
|
||||||
|
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(new StateProvider());
|
||||||
|
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, [restoreKey]);
|
||||||
|
|
||||||
|
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
expect(infoMock).toHaveBeenCalledWith(
|
||||||
|
`Cache not found for input keys: ${key}, ${restoreKey}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("restore 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 stateMock = jest.spyOn(core, "saveState");
|
||||||
|
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
|
||||||
|
const restoreCacheMock = jest
|
||||||
|
.spyOn(cache, "restoreCache")
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return Promise.resolve(key);
|
||||||
|
});
|
||||||
|
|
||||||
|
await run(new StateProvider());
|
||||||
|
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
||||||
|
|
||||||
|
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||||
|
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true");
|
||||||
|
|
||||||
|
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("restore 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 stateMock = jest.spyOn(core, "saveState");
|
||||||
|
const setCacheHitOutputMock = jest.spyOn(core, "setOutput");
|
||||||
|
const restoreCacheMock = jest
|
||||||
|
.spyOn(cache, "restoreCache")
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return Promise.resolve(restoreKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
await run(new StateProvider());
|
||||||
|
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, [restoreKey]);
|
||||||
|
|
||||||
|
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||||
|
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false");
|
||||||
|
expect(infoMock).toHaveBeenCalledWith(
|
||||||
|
`Cache restored from key: ${restoreKey}`
|
||||||
|
);
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
177
__tests__/restoreOnly.test.ts
Normal file
177
__tests__/restoreOnly.test.ts
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
import * as cache from "@actions/cache";
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
|
import { Events, RefKey } from "../src/constants";
|
||||||
|
import run from "../src/restoreOnly";
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
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("restore 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, []);
|
||||||
|
|
||||||
|
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("restore 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]
|
||||||
|
});
|
||||||
|
|
||||||
|
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]);
|
||||||
|
|
||||||
|
expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key);
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
|
||||||
|
expect(infoMock).toHaveBeenCalledWith(
|
||||||
|
`Cache not found for input keys: ${key}, ${restoreKey}`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("restore 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, []);
|
||||||
|
|
||||||
|
expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key);
|
||||||
|
expect(outputMock).toHaveBeenCalledWith("cache-hit", "true");
|
||||||
|
expect(outputMock).toHaveBeenCalledWith("cache-restore-key", key);
|
||||||
|
|
||||||
|
expect(outputMock).toHaveBeenCalledTimes(3);
|
||||||
|
|
||||||
|
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("restore 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]);
|
||||||
|
|
||||||
|
expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key);
|
||||||
|
expect(outputMock).toHaveBeenCalledWith("cache-hit", "false");
|
||||||
|
expect(outputMock).toHaveBeenCalledWith("cache-restore-key", restoreKey);
|
||||||
|
|
||||||
|
expect(outputMock).toHaveBeenCalledTimes(3);
|
||||||
|
|
||||||
|
expect(infoMock).toHaveBeenCalledWith(
|
||||||
|
`Cache restored from key: ${restoreKey}`
|
||||||
|
);
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
|
@ -15,8 +15,8 @@ beforeAll(() => {
|
||||||
return jest.requireActual("@actions/core").getInput(name, options);
|
return jest.requireActual("@actions/core").getInput(name, options);
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.spyOn(actionUtils, "getCacheState").mockImplementation(() => {
|
jest.spyOn(core, "getState").mockImplementation(name => {
|
||||||
return jest.requireActual("../src/utils/actionUtils").getCacheState();
|
return jest.requireActual("@actions/core").getState(name);
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
|
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
|
||||||
|
@ -65,294 +65,6 @@ afterEach(() => {
|
||||||
delete process.env[RefKey];
|
delete process.env[RefKey];
|
||||||
});
|
});
|
||||||
|
|
||||||
test("save with invalid event outputs warning", async () => {
|
|
||||||
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
|
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
|
||||||
const invalidEvent = "commit_comment";
|
|
||||||
process.env[Events.Key] = invalidEvent;
|
|
||||||
delete process.env[RefKey];
|
|
||||||
await run();
|
|
||||||
expect(logWarningMock).toHaveBeenCalledWith(
|
|
||||||
`Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
|
|
||||||
);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("save with no primary key in state outputs warning", async () => {
|
|
||||||
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
|
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
|
||||||
|
|
||||||
const savedCacheKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
|
||||||
jest.spyOn(core, "getState")
|
|
||||||
// Cache Entry State
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return savedCacheKey;
|
|
||||||
})
|
|
||||||
// Cache Key State
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
|
||||||
expect(logWarningMock).toHaveBeenCalledWith(
|
|
||||||
`Error retrieving key from state.`
|
|
||||||
);
|
|
||||||
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("save without AC available should no-op", async () => {
|
|
||||||
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
|
|
||||||
() => false
|
|
||||||
);
|
|
||||||
|
|
||||||
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("save on ghes without AC available should no-op", async () => {
|
|
||||||
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
|
|
||||||
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
|
|
||||||
() => false
|
|
||||||
);
|
|
||||||
|
|
||||||
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("save on GHES with AC available", async () => {
|
|
||||||
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
|
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
|
||||||
|
|
||||||
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
|
||||||
const savedCacheKey = "Linux-node-";
|
|
||||||
|
|
||||||
jest.spyOn(core, "getState")
|
|
||||||
// Cache Entry State
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return savedCacheKey;
|
|
||||||
})
|
|
||||||
// Cache Key State
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return primaryKey;
|
|
||||||
});
|
|
||||||
|
|
||||||
const inputPath = "node_modules";
|
|
||||||
testUtils.setInput(Inputs.Path, inputPath);
|
|
||||||
testUtils.setInput(Inputs.UploadChunkSize, "4000000");
|
|
||||||
|
|
||||||
const cacheId = 4;
|
|
||||||
const saveCacheMock = jest
|
|
||||||
.spyOn(cache, "saveCache")
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return Promise.resolve(cacheId);
|
|
||||||
});
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith([inputPath], primaryKey, {
|
|
||||||
uploadChunkSize: 4000000
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("save with exact match returns early", async () => {
|
|
||||||
const infoMock = jest.spyOn(core, "info");
|
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
|
||||||
|
|
||||||
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
|
||||||
const savedCacheKey = primaryKey;
|
|
||||||
|
|
||||||
jest.spyOn(core, "getState")
|
|
||||||
// Cache Entry State
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return savedCacheKey;
|
|
||||||
})
|
|
||||||
// Cache Key State
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return primaryKey;
|
|
||||||
});
|
|
||||||
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
|
||||||
expect(infoMock).toHaveBeenCalledWith(
|
|
||||||
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
|
|
||||||
);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("save with missing input outputs 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")
|
|
||||||
// Cache Entry State
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return savedCacheKey;
|
|
||||||
})
|
|
||||||
// Cache Key State
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return primaryKey;
|
|
||||||
});
|
|
||||||
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
|
||||||
expect(logWarningMock).toHaveBeenCalledWith(
|
|
||||||
"Input required and not supplied: path"
|
|
||||||
);
|
|
||||||
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("save with large cache outputs 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")
|
|
||||||
// Cache Entry State
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return savedCacheKey;
|
|
||||||
})
|
|
||||||
// Cache Key State
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return primaryKey;
|
|
||||||
});
|
|
||||||
|
|
||||||
const inputPath = "node_modules";
|
|
||||||
testUtils.setInput(Inputs.Path, inputPath);
|
|
||||||
|
|
||||||
const saveCacheMock = jest
|
|
||||||
.spyOn(cache, "saveCache")
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
throw new Error(
|
|
||||||
"Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(
|
|
||||||
[inputPath],
|
|
||||||
primaryKey,
|
|
||||||
expect.anything()
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(logWarningMock).toHaveBeenCalledWith(
|
|
||||||
"Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
|
|
||||||
);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("save with reserve cache failure outputs 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")
|
|
||||||
// Cache Entry State
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return savedCacheKey;
|
|
||||||
})
|
|
||||||
// Cache Key State
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return primaryKey;
|
|
||||||
});
|
|
||||||
|
|
||||||
const inputPath = "node_modules";
|
|
||||||
testUtils.setInput(Inputs.Path, inputPath);
|
|
||||||
|
|
||||||
const saveCacheMock = jest
|
|
||||||
.spyOn(cache, "saveCache")
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
const actualCache = jest.requireActual("@actions/cache");
|
|
||||||
const error = new actualCache.ReserveCacheError(
|
|
||||||
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
|
|
||||||
);
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(
|
|
||||||
[inputPath],
|
|
||||||
primaryKey,
|
|
||||||
expect.anything()
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(logWarningMock).toHaveBeenCalledWith(
|
|
||||||
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
|
|
||||||
);
|
|
||||||
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("save with server error outputs 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")
|
|
||||||
// Cache Entry State
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return savedCacheKey;
|
|
||||||
})
|
|
||||||
// Cache Key State
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return primaryKey;
|
|
||||||
});
|
|
||||||
|
|
||||||
const inputPath = "node_modules";
|
|
||||||
testUtils.setInput(Inputs.Path, inputPath);
|
|
||||||
|
|
||||||
const saveCacheMock = jest
|
|
||||||
.spyOn(cache, "saveCache")
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
throw new Error("HTTP Error Occurred");
|
|
||||||
});
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledWith(
|
|
||||||
[inputPath],
|
|
||||||
primaryKey,
|
|
||||||
expect.anything()
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
|
|
||||||
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("save with valid inputs uploads a cache", async () => {
|
test("save with valid inputs uploads a cache", async () => {
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
|
||||||
|
@ -362,11 +74,11 @@ test("save with valid inputs uploads a cache", async () => {
|
||||||
jest.spyOn(core, "getState")
|
jest.spyOn(core, "getState")
|
||||||
// Cache Entry State
|
// Cache Entry State
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
return savedCacheKey;
|
return primaryKey;
|
||||||
})
|
})
|
||||||
// Cache Key State
|
// Cache Key State
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
return primaryKey;
|
return savedCacheKey;
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputPath = "node_modules";
|
const inputPath = "node_modules";
|
||||||
|
|
386
__tests__/saveImpl.test.ts
Normal file
386
__tests__/saveImpl.test.ts
Normal file
|
@ -0,0 +1,386 @@
|
||||||
|
import * as cache from "@actions/cache";
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
|
import { Events, Inputs, RefKey } from "../src/constants";
|
||||||
|
import run from "../src/saveImpl";
|
||||||
|
import { StateProvider } from "../src/stateProvider";
|
||||||
|
import * as actionUtils from "../src/utils/actionUtils";
|
||||||
|
import * as testUtils from "../src/utils/testUtils";
|
||||||
|
|
||||||
|
jest.mock("@actions/core");
|
||||||
|
jest.mock("@actions/cache");
|
||||||
|
jest.mock("../src/utils/actionUtils");
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
|
||||||
|
return jest.requireActual("@actions/core").getInput(name, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
|
||||||
|
(name, options) => {
|
||||||
|
return jest
|
||||||
|
.requireActual("../src/utils/actionUtils")
|
||||||
|
.getInputAsArray(name, options);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.spyOn(actionUtils, "getInputAsInt").mockImplementation(
|
||||||
|
(name, options) => {
|
||||||
|
return jest
|
||||||
|
.requireActual("../src/utils/actionUtils")
|
||||||
|
.getInputAsInt(name, options);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
|
||||||
|
(key, cacheResult) => {
|
||||||
|
return jest
|
||||||
|
.requireActual("../src/utils/actionUtils")
|
||||||
|
.isExactKeyMatch(key, cacheResult);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
|
||||||
|
const actualUtils = jest.requireActual("../src/utils/actionUtils");
|
||||||
|
return actualUtils.isValidEvent();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
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("save with invalid event outputs warning", async () => {
|
||||||
|
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
|
||||||
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
const invalidEvent = "commit_comment";
|
||||||
|
process.env[Events.Key] = invalidEvent;
|
||||||
|
delete process.env[RefKey];
|
||||||
|
await run(new StateProvider());
|
||||||
|
expect(logWarningMock).toHaveBeenCalledWith(
|
||||||
|
`Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
|
||||||
|
);
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("save with no primary key in state outputs warning", async () => {
|
||||||
|
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
|
||||||
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
|
||||||
|
const savedCacheKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
||||||
|
jest.spyOn(core, "getState")
|
||||||
|
// Cache Entry State
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return "";
|
||||||
|
})
|
||||||
|
// Cache Key State
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return savedCacheKey;
|
||||||
|
});
|
||||||
|
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
||||||
|
|
||||||
|
await run(new StateProvider());
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||||
|
expect(logWarningMock).toHaveBeenCalledWith(`Key is not specified.`);
|
||||||
|
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("save without AC available should no-op", async () => {
|
||||||
|
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
|
||||||
|
() => false
|
||||||
|
);
|
||||||
|
|
||||||
|
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
||||||
|
|
||||||
|
await run(new StateProvider());
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("save on ghes without AC available should no-op", async () => {
|
||||||
|
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
|
||||||
|
jest.spyOn(actionUtils, "isCacheFeatureAvailable").mockImplementation(
|
||||||
|
() => false
|
||||||
|
);
|
||||||
|
|
||||||
|
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
||||||
|
|
||||||
|
await run(new StateProvider());
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("save on GHES with AC available", async () => {
|
||||||
|
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
|
||||||
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
|
||||||
|
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
||||||
|
const savedCacheKey = "Linux-node-";
|
||||||
|
|
||||||
|
jest.spyOn(core, "getState")
|
||||||
|
// Cache Entry State
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return savedCacheKey;
|
||||||
|
})
|
||||||
|
// Cache Key State
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return primaryKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputPath = "node_modules";
|
||||||
|
testUtils.setInput(Inputs.Path, inputPath);
|
||||||
|
testUtils.setInput(Inputs.UploadChunkSize, "4000000");
|
||||||
|
|
||||||
|
const cacheId = 4;
|
||||||
|
const saveCacheMock = jest
|
||||||
|
.spyOn(cache, "saveCache")
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return Promise.resolve(cacheId);
|
||||||
|
});
|
||||||
|
|
||||||
|
await run(new StateProvider());
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledWith([inputPath], primaryKey, {
|
||||||
|
uploadChunkSize: 4000000
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("save with exact match returns early", async () => {
|
||||||
|
const infoMock = jest.spyOn(core, "info");
|
||||||
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
|
||||||
|
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
||||||
|
const savedCacheKey = primaryKey;
|
||||||
|
|
||||||
|
jest.spyOn(core, "getState")
|
||||||
|
// Cache Entry State
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return savedCacheKey;
|
||||||
|
})
|
||||||
|
// Cache Key State
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return primaryKey;
|
||||||
|
});
|
||||||
|
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
||||||
|
|
||||||
|
await run(new StateProvider());
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||||
|
expect(infoMock).toHaveBeenCalledWith(
|
||||||
|
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
|
||||||
|
);
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("save with missing input outputs 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")
|
||||||
|
// Cache Entry State
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return savedCacheKey;
|
||||||
|
})
|
||||||
|
// Cache Key State
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return primaryKey;
|
||||||
|
});
|
||||||
|
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
||||||
|
|
||||||
|
await run(new StateProvider());
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||||
|
expect(logWarningMock).toHaveBeenCalledWith(
|
||||||
|
"Input required and not supplied: path"
|
||||||
|
);
|
||||||
|
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("save with large cache outputs 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")
|
||||||
|
// Cache Entry State
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return savedCacheKey;
|
||||||
|
})
|
||||||
|
// Cache Key State
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return primaryKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputPath = "node_modules";
|
||||||
|
testUtils.setInput(Inputs.Path, inputPath);
|
||||||
|
|
||||||
|
const saveCacheMock = jest
|
||||||
|
.spyOn(cache, "saveCache")
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new Error(
|
||||||
|
"Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
await run(new StateProvider());
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledWith(
|
||||||
|
[inputPath],
|
||||||
|
primaryKey,
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(logWarningMock).toHaveBeenCalledWith(
|
||||||
|
"Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
|
||||||
|
);
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("save with reserve cache failure outputs 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")
|
||||||
|
// Cache Entry State
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return savedCacheKey;
|
||||||
|
})
|
||||||
|
// Cache Key State
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return primaryKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputPath = "node_modules";
|
||||||
|
testUtils.setInput(Inputs.Path, inputPath);
|
||||||
|
|
||||||
|
const saveCacheMock = jest
|
||||||
|
.spyOn(cache, "saveCache")
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
const actualCache = jest.requireActual("@actions/cache");
|
||||||
|
const error = new actualCache.ReserveCacheError(
|
||||||
|
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
|
await run(new StateProvider());
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledWith(
|
||||||
|
[inputPath],
|
||||||
|
primaryKey,
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logWarningMock).toHaveBeenCalledWith(
|
||||||
|
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
|
||||||
|
);
|
||||||
|
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("save with server error outputs 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")
|
||||||
|
// Cache Entry State
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return savedCacheKey;
|
||||||
|
})
|
||||||
|
// Cache Key State
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return primaryKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputPath = "node_modules";
|
||||||
|
testUtils.setInput(Inputs.Path, inputPath);
|
||||||
|
|
||||||
|
const saveCacheMock = jest
|
||||||
|
.spyOn(cache, "saveCache")
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
throw new Error("HTTP Error Occurred");
|
||||||
|
});
|
||||||
|
|
||||||
|
await run(new StateProvider());
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledWith(
|
||||||
|
[inputPath],
|
||||||
|
primaryKey,
|
||||||
|
expect.anything()
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
|
||||||
|
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("save with valid inputs uploads a cache", async () => {
|
||||||
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
|
||||||
|
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
||||||
|
const savedCacheKey = "Linux-node-";
|
||||||
|
|
||||||
|
jest.spyOn(core, "getState")
|
||||||
|
// Cache Entry State
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return savedCacheKey;
|
||||||
|
})
|
||||||
|
// Cache Key State
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return primaryKey;
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputPath = "node_modules";
|
||||||
|
testUtils.setInput(Inputs.Path, inputPath);
|
||||||
|
testUtils.setInput(Inputs.UploadChunkSize, "4000000");
|
||||||
|
|
||||||
|
const cacheId = 4;
|
||||||
|
const saveCacheMock = jest
|
||||||
|
.spyOn(cache, "saveCache")
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return Promise.resolve(cacheId);
|
||||||
|
});
|
||||||
|
|
||||||
|
await run(new StateProvider());
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledWith([inputPath], primaryKey, {
|
||||||
|
uploadChunkSize: 4000000
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
93
__tests__/saveOnly.test.ts
Normal file
93
__tests__/saveOnly.test.ts
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
import * as cache from "@actions/cache";
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
|
import { Events, Inputs, RefKey } from "../src/constants";
|
||||||
|
import run from "../src/saveOnly";
|
||||||
|
import * as actionUtils from "../src/utils/actionUtils";
|
||||||
|
import * as testUtils from "../src/utils/testUtils";
|
||||||
|
|
||||||
|
jest.mock("@actions/core");
|
||||||
|
jest.mock("@actions/cache");
|
||||||
|
jest.mock("../src/utils/actionUtils");
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
|
||||||
|
return jest.requireActual("@actions/core").getInput(name, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(core, "setOutput").mockImplementation((key, value) => {
|
||||||
|
return jest.requireActual("@actions/core").getInput(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
|
||||||
|
(name, options) => {
|
||||||
|
return jest
|
||||||
|
.requireActual("../src/utils/actionUtils")
|
||||||
|
.getInputAsArray(name, options);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.spyOn(actionUtils, "getInputAsInt").mockImplementation(
|
||||||
|
(name, options) => {
|
||||||
|
return jest
|
||||||
|
.requireActual("../src/utils/actionUtils")
|
||||||
|
.getInputAsInt(name, options);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
|
||||||
|
(key, cacheResult) => {
|
||||||
|
return jest
|
||||||
|
.requireActual("../src/utils/actionUtils")
|
||||||
|
.isExactKeyMatch(key, cacheResult);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
|
||||||
|
const actualUtils = jest.requireActual("../src/utils/actionUtils");
|
||||||
|
return actualUtils.isValidEvent();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
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("save with valid inputs uploads a cache", async () => {
|
||||||
|
const failedMock = jest.spyOn(core, "setFailed");
|
||||||
|
|
||||||
|
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
||||||
|
|
||||||
|
const inputPath = "node_modules";
|
||||||
|
testUtils.setInput(Inputs.Key, primaryKey);
|
||||||
|
testUtils.setInput(Inputs.Path, inputPath);
|
||||||
|
testUtils.setInput(Inputs.UploadChunkSize, "4000000");
|
||||||
|
|
||||||
|
const cacheId = 4;
|
||||||
|
const saveCacheMock = jest
|
||||||
|
.spyOn(cache, "saveCache")
|
||||||
|
.mockImplementationOnce(() => {
|
||||||
|
return Promise.resolve(cacheId);
|
||||||
|
});
|
||||||
|
|
||||||
|
await run();
|
||||||
|
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(saveCacheMock).toHaveBeenCalledWith([inputPath], primaryKey, {
|
||||||
|
uploadChunkSize: 4000000
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
71
__tests__/stateProvider.test.ts
Normal file
71
__tests__/stateProvider.test.ts
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
|
import { Events, RefKey, State } from "../src/constants";
|
||||||
|
import {
|
||||||
|
IStateProvider,
|
||||||
|
NullStateProvider,
|
||||||
|
StateProvider
|
||||||
|
} from "../src/stateProvider";
|
||||||
|
|
||||||
|
jest.mock("@actions/core");
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
|
||||||
|
return jest.requireActual("@actions/core").getInput(name, options);
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.spyOn(core, "setOutput").mockImplementation((key, value) => {
|
||||||
|
return jest.requireActual("@actions/core").setOutput(key, value);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete process.env[Events.Key];
|
||||||
|
delete process.env[RefKey];
|
||||||
|
});
|
||||||
|
|
||||||
|
test("StateProvider saves states", async () => {
|
||||||
|
const getStateMock = jest
|
||||||
|
.spyOn(core, "getState")
|
||||||
|
.mockImplementation(name =>
|
||||||
|
jest.requireActual("@actions/core").getState(name)
|
||||||
|
);
|
||||||
|
const saveStateMock = jest
|
||||||
|
.spyOn(core, "saveState")
|
||||||
|
.mockImplementation((key, value) => {
|
||||||
|
return jest.requireActual("@actions/core").saveState(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const cacheMatchedKey = "node-cache";
|
||||||
|
|
||||||
|
const stateProvider: IStateProvider = new StateProvider();
|
||||||
|
stateProvider.setState("stateKey", "stateValue");
|
||||||
|
stateProvider.setState(State.CacheMatchedKey, cacheMatchedKey);
|
||||||
|
stateProvider.getState("stateKey");
|
||||||
|
stateProvider.getCacheState();
|
||||||
|
|
||||||
|
expect(getStateMock).toHaveBeenCalledTimes(2);
|
||||||
|
expect(saveStateMock).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("NullStateProvider saves outputs", async () => {
|
||||||
|
const getStateMock = jest
|
||||||
|
.spyOn(core, "getState")
|
||||||
|
.mockImplementation(name =>
|
||||||
|
jest.requireActual("@actions/core").getState(name)
|
||||||
|
);
|
||||||
|
const setOutputMock = jest
|
||||||
|
.spyOn(core, "setOutput")
|
||||||
|
.mockImplementation((key, value) => {
|
||||||
|
return jest.requireActual("@actions/core").setOutput(key, value);
|
||||||
|
});
|
||||||
|
const cacheMatchedKey = "node-cache";
|
||||||
|
const nullStateProvider: IStateProvider = new NullStateProvider();
|
||||||
|
nullStateProvider.setState(State.CacheMatchedKey, "outputValue");
|
||||||
|
nullStateProvider.setState(State.CachePrimaryKey, cacheMatchedKey);
|
||||||
|
nullStateProvider.getState("outputKey");
|
||||||
|
nullStateProvider.getCacheState();
|
||||||
|
|
||||||
|
expect(getStateMock).toHaveBeenCalledTimes(0);
|
||||||
|
expect(setOutputMock).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
10
action.yml
10
action.yml
|
@ -14,14 +14,6 @@ inputs:
|
||||||
upload-chunk-size:
|
upload-chunk-size:
|
||||||
description: 'The chunk size used to split up large files during upload, in bytes'
|
description: 'The chunk size used to split up large files during upload, in bytes'
|
||||||
required: false
|
required: false
|
||||||
exit-on-cache-miss:
|
|
||||||
description: 'Fail the workflow if the cache is not found for the primary key'
|
|
||||||
required: false
|
|
||||||
default: false
|
|
||||||
save-on-any-failure:
|
|
||||||
description: 'Save cache (on cache miss) despite of any failure during the workflow run'
|
|
||||||
required: false
|
|
||||||
default: 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'
|
||||||
|
@ -29,7 +21,7 @@ runs:
|
||||||
using: 'node16'
|
using: 'node16'
|
||||||
main: 'dist/restore/index.js'
|
main: 'dist/restore/index.js'
|
||||||
post: 'dist/save/index.js'
|
post: 'dist/save/index.js'
|
||||||
post-if: (success() || (env.SAVE_CACHE_ON_ANY_FAILURE == 'yes'))
|
post-if: success()
|
||||||
branding:
|
branding:
|
||||||
icon: 'archive'
|
icon: 'archive'
|
||||||
color: 'gray-dark'
|
color: 'gray-dark'
|
||||||
|
|
61164
dist/restore-only/index.js
vendored
Normal file
61164
dist/restore-only/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
992
dist/restore/index.js
vendored
992
dist/restore/index.js
vendored
File diff suppressed because it is too large
Load diff
61170
dist/save-only/index.js
vendored
Normal file
61170
dist/save-only/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
973
dist/save/index.js
vendored
973
dist/save/index.js
vendored
File diff suppressed because it is too large
Load diff
21
examples.md
21
examples.md
|
@ -309,14 +309,29 @@ We cache the elements of the Cabal store separately, as the entirety of `~/.caba
|
||||||
For npm, cache files are stored in `~/.npm` on Posix, or `~\AppData\npm-cache` on Windows, but it's possible to use `npm config get cache` to find the path on any platform. See [the npm docs](https://docs.npmjs.com/cli/cache#cache) for more details.
|
For npm, cache files are stored in `~/.npm` on Posix, or `~\AppData\npm-cache` on Windows, but it's possible to use `npm config get cache` to find the path on any platform. See [the npm docs](https://docs.npmjs.com/cli/cache#cache) for more details.
|
||||||
|
|
||||||
If using `npm config` to retrieve the cache directory, ensure you run [actions/setup-node](https://github.com/actions/setup-node) first to ensure your `npm` version is correct.
|
If using `npm config` to retrieve the cache directory, ensure you run [actions/setup-node](https://github.com/actions/setup-node) first to ensure your `npm` version is correct.
|
||||||
|
After [deprecation](https://github.blog/changelog/2022-10-11-github-actions-deprecating-save-state-and-set-output-commands/) of save-state and set-output commands, the correct way to set output is using `${GITHUB_OUTPUT}`. For linux, we can use `${GITHUB_OUTPUT}` whereas for windows we need to use `${env:GITHUB_OUTPUT}` due to two different default shells in these two different OS ie `bash` and `pwsh` respectively.
|
||||||
|
|
||||||
>Note: It is not recommended to cache `node_modules`, as it can break across Node versions and won't work with `npm ci`
|
>Note: It is not recommended to cache `node_modules`, as it can break across Node versions and won't work with `npm ci`
|
||||||
|
|
||||||
|
### **Get npm cache directory using same shell**
|
||||||
|
### Bash shell
|
||||||
```yaml
|
```yaml
|
||||||
- name: Get npm cache directory
|
- name: Get npm cache directory
|
||||||
id: npm-cache-dir
|
id: npm-cache
|
||||||
run: |
|
shell: bash
|
||||||
echo "::set-output name=dir::$(npm config get cache)"
|
run: echo "dir=$(npm config get cache)" >> ${GITHUB_OUTPUT}
|
||||||
|
```
|
||||||
|
|
||||||
|
### PWSH shell
|
||||||
|
```yaml
|
||||||
|
- name: Get npm cache directory
|
||||||
|
id: npm-cache
|
||||||
|
shell: pwsh
|
||||||
|
run: echo "dir=$(npm config get cache)" >> ${env:GITHUB_OUTPUT}
|
||||||
|
```
|
||||||
|
`Get npm cache directory` step can then be used with `actions/cache` as shown below
|
||||||
|
|
||||||
|
```yaml
|
||||||
- uses: actions/cache@v3
|
- uses: actions/cache@v3
|
||||||
id: npm-cache # use this to check for `cache-hit` ==> if: steps.npm-cache.outputs.cache-hit != 'true'
|
id: npm-cache # use this to check for `cache-hit` ==> if: steps.npm-cache.outputs.cache-hit != 'true'
|
||||||
with:
|
with:
|
||||||
|
|
38
package-lock.json
generated
38
package-lock.json
generated
|
@ -1,15 +1,15 @@
|
||||||
{
|
{
|
||||||
"name": "cache",
|
"name": "cache",
|
||||||
"version": "3.0.11",
|
"version": "3.1.0-beta.2",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "cache",
|
"name": "cache",
|
||||||
"version": "3.0.11",
|
"version": "3.1.0-beta.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/cache": "^3.0.5",
|
"@actions/cache": "3.1.0-beta.2",
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.10.0",
|
||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"@actions/io": "^1.1.2"
|
"@actions/io": "^1.1.2"
|
||||||
|
@ -36,15 +36,16 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@actions/cache": {
|
"node_modules/@actions/cache": {
|
||||||
"version": "3.0.5",
|
"version": "3.1.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/cache/-/cache-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/cache/-/cache-3.1.0-beta.2.tgz",
|
||||||
"integrity": "sha512-0WpPmwnRPkn5k5ASmjoX8bY8NrZEPTwN+64nGYJmR/bHjEVgC8svdf5K956wi67tNJBGJky2+UfvNbUOtHmMHg==",
|
"integrity": "sha512-xt9NLWPCh5WU9Z5ITeGpT5Nza/57wMXeLsGuNVcRCIVpPuNTf3Puj82vjZZQw4rGqiCCs+n4+hnkTcE9BKw2sw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.10.0",
|
||||||
"@actions/exec": "^1.0.1",
|
"@actions/exec": "^1.0.1",
|
||||||
"@actions/glob": "^0.1.0",
|
"@actions/glob": "^0.1.0",
|
||||||
"@actions/http-client": "^2.0.1",
|
"@actions/http-client": "^2.0.1",
|
||||||
"@actions/io": "^1.0.1",
|
"@actions/io": "^1.0.1",
|
||||||
|
"@azure/abort-controller": "^1.1.0",
|
||||||
"@azure/ms-rest-js": "^2.6.0",
|
"@azure/ms-rest-js": "^2.6.0",
|
||||||
"@azure/storage-blob": "^12.8.0",
|
"@azure/storage-blob": "^12.8.0",
|
||||||
"semver": "^6.1.0",
|
"semver": "^6.1.0",
|
||||||
|
@ -111,14 +112,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/abort-controller": {
|
"node_modules/@azure/abort-controller": {
|
||||||
"version": "1.0.4",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz",
|
||||||
"integrity": "sha512-lNUmDRVGpanCsiUN3NWxFTdwmdFI53xwhkTFfHDGTYk46ca7Ind3nanJc+U6Zj9Tv+9nTCWRBscWEW1DyKOpTw==",
|
"integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@azure/abort-controller/node_modules/tslib": {
|
"node_modules/@azure/abort-controller/node_modules/tslib": {
|
||||||
|
@ -9721,15 +9722,16 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/cache": {
|
"@actions/cache": {
|
||||||
"version": "3.0.5",
|
"version": "3.1.0-beta.2",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/cache/-/cache-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/cache/-/cache-3.1.0-beta.2.tgz",
|
||||||
"integrity": "sha512-0WpPmwnRPkn5k5ASmjoX8bY8NrZEPTwN+64nGYJmR/bHjEVgC8svdf5K956wi67tNJBGJky2+UfvNbUOtHmMHg==",
|
"integrity": "sha512-xt9NLWPCh5WU9Z5ITeGpT5Nza/57wMXeLsGuNVcRCIVpPuNTf3Puj82vjZZQw4rGqiCCs+n4+hnkTcE9BKw2sw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.10.0",
|
||||||
"@actions/exec": "^1.0.1",
|
"@actions/exec": "^1.0.1",
|
||||||
"@actions/glob": "^0.1.0",
|
"@actions/glob": "^0.1.0",
|
||||||
"@actions/http-client": "^2.0.1",
|
"@actions/http-client": "^2.0.1",
|
||||||
"@actions/io": "^1.0.1",
|
"@actions/io": "^1.0.1",
|
||||||
|
"@azure/abort-controller": "^1.1.0",
|
||||||
"@azure/ms-rest-js": "^2.6.0",
|
"@azure/ms-rest-js": "^2.6.0",
|
||||||
"@azure/storage-blob": "^12.8.0",
|
"@azure/storage-blob": "^12.8.0",
|
||||||
"semver": "^6.1.0",
|
"semver": "^6.1.0",
|
||||||
|
@ -9792,11 +9794,11 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@azure/abort-controller": {
|
"@azure/abort-controller": {
|
||||||
"version": "1.0.4",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz",
|
||||||
"integrity": "sha512-lNUmDRVGpanCsiUN3NWxFTdwmdFI53xwhkTFfHDGTYk46ca7Ind3nanJc+U6Zj9Tv+9nTCWRBscWEW1DyKOpTw==",
|
"integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"tslib": "^2.0.0"
|
"tslib": "^2.2.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": {
|
"tslib": {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "cache",
|
"name": "cache",
|
||||||
"version": "3.0.11",
|
"version": "3.1.0-beta.2",
|
||||||
"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",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc && ncc build -o dist/restore src/restore.ts && ncc build -o dist/save src/save.ts",
|
"build": "tsc && ncc build -o dist/restore src/restore.ts && ncc build -o dist/save src/save.ts && ncc build -o dist/restore-only src/restoreOnly.ts && ncc build -o dist/save-only src/saveOnly.ts",
|
||||||
"test": "tsc --noEmit && jest --coverage",
|
"test": "tsc --noEmit && jest --coverage",
|
||||||
"lint": "eslint **/*.ts --cache",
|
"lint": "eslint **/*.ts --cache",
|
||||||
"format": "prettier --write **/*.ts",
|
"format": "prettier --write **/*.ts",
|
||||||
|
@ -23,7 +23,7 @@
|
||||||
"author": "GitHub",
|
"author": "GitHub",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/cache": "^3.0.5",
|
"@actions/cache": "3.1.0-beta.2",
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.10.0",
|
||||||
"@actions/exec": "^1.1.1",
|
"@actions/exec": "^1.1.1",
|
||||||
"@actions/io": "^1.1.2"
|
"@actions/io": "^1.1.2"
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
name: 'Restore Cache'
|
name: 'Restore Only Cache'
|
||||||
description: 'Restore Cache artifacts like dependencies and build outputs to improve workflow execution time'
|
description: 'Restore Cache artifacts like dependencies and build outputs to improve workflow execution time'
|
||||||
author: 'GitHub'
|
author: 'GitHub'
|
||||||
inputs:
|
inputs:
|
||||||
|
@ -11,17 +11,16 @@ inputs:
|
||||||
restore-keys:
|
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.'
|
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
|
required: false
|
||||||
exit-on-cache-miss:
|
|
||||||
description: 'Fail the workflow if the cache is not found for the primary key'
|
|
||||||
required: false
|
|
||||||
default: 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'
|
||||||
|
cache-primary-key:
|
||||||
|
description: 'Cache primary key passed in the input to use in subsequent steps of the workflow'
|
||||||
|
cache-restore-key:
|
||||||
|
description: 'Cache key restored'
|
||||||
runs:
|
runs:
|
||||||
using: 'node16'
|
using: 'node16'
|
||||||
main: '../dist/restore/index.js'
|
main: '../dist/restore-only/index.js'
|
||||||
branding:
|
branding:
|
||||||
icon: 'archive'
|
icon: 'archive'
|
||||||
color: 'gray-dark'
|
color: 'gray-dark'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
name: 'Save Cache'
|
name: 'Save Only Cache'
|
||||||
description: 'Save Cache artifacts like dependencies and build outputs to improve workflow execution time'
|
description: 'Save Cache artifacts like dependencies and build outputs to improve workflow execution time'
|
||||||
author: 'GitHub'
|
author: 'GitHub'
|
||||||
inputs:
|
inputs:
|
||||||
|
@ -13,7 +13,7 @@ inputs:
|
||||||
required: false
|
required: false
|
||||||
runs:
|
runs:
|
||||||
using: 'node16'
|
using: 'node16'
|
||||||
main: '../dist/save/index.js'
|
main: '../dist/save-only/index.js'
|
||||||
branding:
|
branding:
|
||||||
icon: 'archive'
|
icon: 'archive'
|
||||||
color: 'gray-dark'
|
color: 'gray-dark'
|
|
@ -1,14 +1,14 @@
|
||||||
export enum Inputs {
|
export enum Inputs {
|
||||||
Key = "key",
|
Key = "key", // Input for cache, restore, save action
|
||||||
Path = "path",
|
Path = "path", // Input for cache, restore, save action
|
||||||
RestoreKeys = "restore-keys",
|
RestoreKeys = "restore-keys", // Input for cache, restore action
|
||||||
UploadChunkSize = "upload-chunk-size",
|
UploadChunkSize = "upload-chunk-size" // Input for cache, save action
|
||||||
FailOnCacheMiss = "fail-on-cache-miss",
|
|
||||||
SaveOnAnyFailure = "save-on-any-failure"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Outputs {
|
export enum Outputs {
|
||||||
CacheHit = "cache-hit"
|
CacheHit = "cache-hit", // Output from cache, restore action
|
||||||
|
CachePrimaryKey = "cache-primary-key", // Output from restore action
|
||||||
|
CacheRestoreKey = "cache-restore-key" // Output from restore action
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum State {
|
export enum State {
|
||||||
|
@ -22,8 +22,4 @@ export enum Events {
|
||||||
PullRequest = "pull_request"
|
PullRequest = "pull_request"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Variables {
|
|
||||||
SaveCacheOnAnyFailure = "SAVE_CACHE_ON_ANY_FAILURE"
|
|
||||||
}
|
|
||||||
|
|
||||||
export const RefKey = "GITHUB_REF";
|
export const RefKey = "GITHUB_REF";
|
||||||
|
|
|
@ -1,84 +1,8 @@
|
||||||
import * as cache from "@actions/cache";
|
import restoreImpl from "./restoreImpl";
|
||||||
import * as core from "@actions/core";
|
import { StateProvider } from "./stateProvider";
|
||||||
|
|
||||||
import { Events, Inputs, State, Variables } from "./constants";
|
|
||||||
import * as utils from "./utils/actionUtils";
|
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
await restoreImpl(new StateProvider());
|
||||||
if (!utils.isCacheFeatureAvailable()) {
|
|
||||||
utils.setCacheHitOutput(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate inputs, this can cause task failure
|
|
||||||
if (!utils.isValidEvent()) {
|
|
||||||
utils.logWarning(
|
|
||||||
`Event Validation Error: The event type ${
|
|
||||||
process.env[Events.Key]
|
|
||||||
} is not supported because it's not tied to a branch or tag ref.`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const primaryKey = core.getInput(Inputs.Key, { required: true });
|
|
||||||
core.saveState(State.CachePrimaryKey, primaryKey);
|
|
||||||
|
|
||||||
const restoreKeys = utils.getInputAsArray(Inputs.RestoreKeys);
|
|
||||||
const cachePaths = utils.getInputAsArray(Inputs.Path, {
|
|
||||||
required: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const cacheKey = await cache.restoreCache(
|
|
||||||
cachePaths,
|
|
||||||
primaryKey,
|
|
||||||
restoreKeys
|
|
||||||
);
|
|
||||||
|
|
||||||
//Check if user wants to save cache despite of failure in any previous job
|
|
||||||
const saveCache = core.getInput(Inputs.SaveOnAnyFailure).toLowerCase();
|
|
||||||
if (saveCache == "true") {
|
|
||||||
core.debug(
|
|
||||||
`Exporting environment variable ${Variables.SaveCacheOnAnyFailure}`
|
|
||||||
);
|
|
||||||
core.exportVariable(Variables.SaveCacheOnAnyFailure, saveCache);
|
|
||||||
core.info(
|
|
||||||
`Input Variable ${Variables.SaveCacheOnAnyFailure} is set to true, the cache will be saved despite of any failure in the build.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!cacheKey) {
|
|
||||||
if (core.getInput(Inputs.FailOnCacheMiss).toLowerCase() == "true") {
|
|
||||||
throw new Error(
|
|
||||||
`Cache with the given input key ${primaryKey} is not found, hence exiting the workflow as the fail-on-cache-miss requirement is not met.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
core.info(
|
|
||||||
`Cache not found for input keys: ${[
|
|
||||||
primaryKey,
|
|
||||||
...restoreKeys
|
|
||||||
].join(", ")}`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Store the matched cache key
|
|
||||||
utils.setCacheState(cacheKey);
|
|
||||||
|
|
||||||
const isExactKeyMatch = utils.isExactKeyMatch(primaryKey, cacheKey);
|
|
||||||
utils.setCacheHitOutput(isExactKeyMatch);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!isExactKeyMatch &&
|
|
||||||
core.getBooleanInput(Inputs.FailOnCacheMiss) == true
|
|
||||||
) {
|
|
||||||
throw new Error(
|
|
||||||
`Restored cache key doesn't match the given input key ${primaryKey}, hence exiting the workflow as the fail-on-cache-miss requirement is not met.`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
core.info(`Cache restored from key: ${cacheKey}`);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
core.setFailed((error as Error).message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
run();
|
||||||
|
|
69
src/restoreImpl.ts
Normal file
69
src/restoreImpl.ts
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
import * as cache from "@actions/cache";
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
|
import { Events, Inputs, Outputs, State } from "./constants";
|
||||||
|
import { IStateProvider } from "./stateProvider";
|
||||||
|
import * as utils from "./utils/actionUtils";
|
||||||
|
|
||||||
|
async function restoreImpl(
|
||||||
|
stateProvider: IStateProvider
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
try {
|
||||||
|
if (!utils.isCacheFeatureAvailable()) {
|
||||||
|
utils.setCacheHitOutput(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate inputs, this can cause task failure
|
||||||
|
if (!utils.isValidEvent()) {
|
||||||
|
utils.logWarning(
|
||||||
|
`Event Validation Error: The event type ${
|
||||||
|
process.env[Events.Key]
|
||||||
|
} is not supported because it's not tied to a branch or tag ref.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryKey = core.getInput(Inputs.Key, { required: true });
|
||||||
|
stateProvider.setState(State.CachePrimaryKey, primaryKey);
|
||||||
|
|
||||||
|
const restoreKeys = utils.getInputAsArray(Inputs.RestoreKeys);
|
||||||
|
const cachePaths = utils.getInputAsArray(Inputs.Path, {
|
||||||
|
required: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const cacheKey = await cache.restoreCache(
|
||||||
|
cachePaths,
|
||||||
|
primaryKey,
|
||||||
|
restoreKeys
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!cacheKey) {
|
||||||
|
core.info(
|
||||||
|
`Cache not found for input keys: ${[
|
||||||
|
primaryKey,
|
||||||
|
...restoreKeys
|
||||||
|
].join(", ")}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the matched cache key in states
|
||||||
|
stateProvider.setState(State.CacheMatchedKey, cacheKey);
|
||||||
|
|
||||||
|
const isExactKeyMatch = utils.isExactKeyMatch(
|
||||||
|
core.getInput(Inputs.Key, { required: true }),
|
||||||
|
cacheKey
|
||||||
|
);
|
||||||
|
|
||||||
|
core.setOutput(Outputs.CacheHit, isExactKeyMatch.toString());
|
||||||
|
core.info(`Cache restored from key: ${cacheKey}`);
|
||||||
|
|
||||||
|
return cacheKey;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
core.setFailed((error as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default restoreImpl;
|
10
src/restoreOnly.ts
Normal file
10
src/restoreOnly.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import restoreImpl from "./restoreImpl";
|
||||||
|
import { NullStateProvider } from "./stateProvider";
|
||||||
|
|
||||||
|
async function run(): Promise<void> {
|
||||||
|
await restoreImpl(new NullStateProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
|
|
||||||
|
export default run;
|
59
src/save.ts
59
src/save.ts
|
@ -1,61 +1,8 @@
|
||||||
import * as cache from "@actions/cache";
|
import saveImpl from "./saveImpl";
|
||||||
import * as core from "@actions/core";
|
import { StateProvider } from "./stateProvider";
|
||||||
|
|
||||||
import { Events, Inputs, State } from "./constants";
|
|
||||||
import * as utils from "./utils/actionUtils";
|
|
||||||
|
|
||||||
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
|
|
||||||
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
|
|
||||||
// throw an uncaught exception. Instead of failing this action, just warn.
|
|
||||||
process.on("uncaughtException", e => utils.logWarning(e.message));
|
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
await saveImpl(new StateProvider());
|
||||||
if (!utils.isCacheFeatureAvailable()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!utils.isValidEvent()) {
|
|
||||||
utils.logWarning(
|
|
||||||
`Event Validation Error: The event type ${
|
|
||||||
process.env[Events.Key]
|
|
||||||
} is not supported because it's not tied to a branch or tag ref.`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const state = utils.getCacheState();
|
|
||||||
|
|
||||||
// Inputs are re-evaluted before the post action, so we want the original key used for restore
|
|
||||||
const primaryKey =
|
|
||||||
core.getState(State.CachePrimaryKey) || core.getInput(Inputs.Key);
|
|
||||||
|
|
||||||
if (!primaryKey) {
|
|
||||||
utils.logWarning(`Error retrieving key from state.`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (utils.isExactKeyMatch(primaryKey, state)) {
|
|
||||||
core.info(
|
|
||||||
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cachePaths = utils.getInputAsArray(Inputs.Path, {
|
|
||||||
required: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const cacheId = await cache.saveCache(cachePaths, primaryKey, {
|
|
||||||
uploadChunkSize: utils.getInputAsInt(Inputs.UploadChunkSize)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (cacheId != -1) {
|
|
||||||
core.info(`Cache saved with key: ${primaryKey}`);
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
utils.logWarning((error as Error).message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
run();
|
||||||
|
|
66
src/saveImpl.ts
Normal file
66
src/saveImpl.ts
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
import * as cache from "@actions/cache";
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
|
import { Events, Inputs, State } from "./constants";
|
||||||
|
import { IStateProvider } from "./stateProvider";
|
||||||
|
import * as utils from "./utils/actionUtils";
|
||||||
|
|
||||||
|
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
|
||||||
|
// @actions/toolkit when a failed upload closes the file descriptor causing any in-process reads to
|
||||||
|
// throw an uncaught exception. Instead of failing this action, just warn.
|
||||||
|
process.on("uncaughtException", e => utils.logWarning(e.message));
|
||||||
|
|
||||||
|
async function saveImpl(stateProvider: IStateProvider): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!utils.isCacheFeatureAvailable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!utils.isValidEvent()) {
|
||||||
|
utils.logWarning(
|
||||||
|
`Event Validation Error: The event type ${
|
||||||
|
process.env[Events.Key]
|
||||||
|
} is not supported because it's not tied to a branch or tag ref.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If restore has stored a primary key in state, reuse that
|
||||||
|
// Else re-evaluate from inputs
|
||||||
|
const primaryKey =
|
||||||
|
stateProvider.getState(State.CachePrimaryKey) ||
|
||||||
|
core.getInput(Inputs.Key);
|
||||||
|
|
||||||
|
if (!primaryKey) {
|
||||||
|
utils.logWarning(`Key is not specified.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If matched restore key is same as primary key, then do not save cache
|
||||||
|
// NO-OP in case of SaveOnly action
|
||||||
|
const restoredKey = stateProvider.getCacheState();
|
||||||
|
|
||||||
|
if (utils.isExactKeyMatch(primaryKey, restoredKey)) {
|
||||||
|
core.info(
|
||||||
|
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachePaths = utils.getInputAsArray(Inputs.Path, {
|
||||||
|
required: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const cacheId = await cache.saveCache(cachePaths, primaryKey, {
|
||||||
|
uploadChunkSize: utils.getInputAsInt(Inputs.UploadChunkSize)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (cacheId != -1) {
|
||||||
|
core.info(`Cache saved with key: ${primaryKey}`);
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
utils.logWarning((error as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default saveImpl;
|
10
src/saveOnly.ts
Normal file
10
src/saveOnly.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import saveImpl from "./saveImpl";
|
||||||
|
import { NullStateProvider } from "./stateProvider";
|
||||||
|
|
||||||
|
async function run(): Promise<void> {
|
||||||
|
await saveImpl(new NullStateProvider());
|
||||||
|
}
|
||||||
|
|
||||||
|
run();
|
||||||
|
|
||||||
|
export default run;
|
46
src/stateProvider.ts
Normal file
46
src/stateProvider.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
|
import { Outputs, State } from "./constants";
|
||||||
|
|
||||||
|
export interface IStateProvider {
|
||||||
|
setState(key: string, value: string): void;
|
||||||
|
getState(key: string): string;
|
||||||
|
|
||||||
|
getCacheState(): string | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateProviderBase implements IStateProvider {
|
||||||
|
getCacheState(): string | undefined {
|
||||||
|
const cacheKey = this.getState(State.CacheMatchedKey);
|
||||||
|
if (cacheKey) {
|
||||||
|
core.debug(`Cache state/key: ${cacheKey}`);
|
||||||
|
return cacheKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function
|
||||||
|
setState = (key: string, value: string) => {};
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
getState = (key: string) => "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StateProvider extends StateProviderBase {
|
||||||
|
setState = core.saveState;
|
||||||
|
getState = core.getState;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NullStateProvider extends StateProviderBase {
|
||||||
|
stateToOutputMap = new Map<string, string>([
|
||||||
|
[State.CacheMatchedKey, Outputs.CacheRestoreKey],
|
||||||
|
[State.CachePrimaryKey, Outputs.CachePrimaryKey]
|
||||||
|
]);
|
||||||
|
|
||||||
|
setState = (key: string, value: string) => {
|
||||||
|
core.setOutput(this.stateToOutputMap.get(key) as string, value);
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
getState = (key: string) => "";
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
import * as cache from "@actions/cache";
|
import * as cache from "@actions/cache";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
import { Outputs, RefKey, State } from "../constants";
|
import { Outputs, RefKey } from "../constants";
|
||||||
|
|
||||||
export function isGhes(): boolean {
|
export function isGhes(): boolean {
|
||||||
const ghUrl = new URL(
|
const ghUrl = new URL(
|
||||||
|
@ -19,30 +19,10 @@ export function isExactKeyMatch(key: string, cacheKey?: string): boolean {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setCacheState(state: string): void {
|
|
||||||
core.saveState(State.CacheMatchedKey, state);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setCacheHitOutput(isCacheHit: boolean): void {
|
export function setCacheHitOutput(isCacheHit: boolean): void {
|
||||||
core.setOutput(Outputs.CacheHit, isCacheHit.toString());
|
core.setOutput(Outputs.CacheHit, isCacheHit.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setOutputAndState(key: string, cacheKey?: string): void {
|
|
||||||
setCacheHitOutput(isExactKeyMatch(key, cacheKey));
|
|
||||||
// Store the matched cache key if it exists
|
|
||||||
cacheKey && setCacheState(cacheKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getCacheState(): string | undefined {
|
|
||||||
const cacheKey = core.getState(State.CacheMatchedKey);
|
|
||||||
if (cacheKey) {
|
|
||||||
core.debug(`Cache state/key: ${cacheKey}`);
|
|
||||||
return cacheKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logWarning(message: string): void {
|
export function logWarning(message: string): void {
|
||||||
const warningPrefix = "[warning]";
|
const warningPrefix = "[warning]";
|
||||||
core.info(`${warningPrefix}${message}`);
|
core.info(`${warningPrefix}${message}`);
|
||||||
|
@ -77,19 +57,20 @@ export function getInputAsInt(
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isCacheFeatureAvailable(): boolean {
|
export function isCacheFeatureAvailable(): boolean {
|
||||||
if (!cache.isFeatureAvailable()) {
|
if (cache.isFeatureAvailable()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (isGhes()) {
|
if (isGhes()) {
|
||||||
logWarning(
|
logWarning(
|
||||||
`Cache action is only supported on GHES version >= 3.5. If you are on version >=3.5 Please check with GHES admin if Actions cache service is enabled or not.
|
`Cache action is only supported on GHES version >= 3.5. If you are on version >=3.5 Please check with GHES admin if Actions cache service is enabled or not.
|
||||||
Otherwise please upgrade to GHES version >= 3.5 and If you are also using Github Connect, please unretire the actions/cache namespace before upgrade (see https://docs.github.com/en/enterprise-server@3.5/admin/github-actions/managing-access-to-actions-from-githubcom/enabling-automatic-access-to-githubcom-actions-using-github-connect#automatic-retirement-of-namespaces-for-actions-accessed-on-githubcom)`
|
Otherwise please upgrade to GHES version >= 3.5 and If you are also using Github Connect, please unretire the actions/cache namespace before upgrade (see https://docs.github.com/en/enterprise-server@3.5/admin/github-actions/managing-access-to-actions-from-githubcom/enabling-automatic-access-to-githubcom-actions-using-github-connect#automatic-retirement-of-namespaces-for-actions-accessed-on-githubcom)`
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
logWarning(
|
|
||||||
"An internal error has occurred in cache backend. Please check https://www.githubstatus.com/ for any ongoing issue in actions."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
logWarning(
|
||||||
|
"An internal error has occurred in cache backend. Please check https://www.githubstatus.com/ for any ongoing issue in actions."
|
||||||
|
);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,28 +13,18 @@ interface CacheInput {
|
||||||
path: string;
|
path: string;
|
||||||
key: string;
|
key: string;
|
||||||
restoreKeys?: string[];
|
restoreKeys?: string[];
|
||||||
failOnCacheMiss?: boolean;
|
|
||||||
saveOnAnyFailure?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setInputs(input: CacheInput): void {
|
export function setInputs(input: CacheInput): void {
|
||||||
setInput(Inputs.Path, input.path);
|
setInput(Inputs.Path, input.path);
|
||||||
setInput(Inputs.Key, input.key);
|
setInput(Inputs.Key, input.key);
|
||||||
setInput(Inputs.SaveOnAnyFailure, "false");
|
|
||||||
setInput(Inputs.FailOnCacheMiss, "false");
|
|
||||||
input.restoreKeys &&
|
input.restoreKeys &&
|
||||||
setInput(Inputs.RestoreKeys, input.restoreKeys.join("\n"));
|
setInput(Inputs.RestoreKeys, input.restoreKeys.join("\n"));
|
||||||
input.failOnCacheMiss &&
|
|
||||||
setInput(Inputs.FailOnCacheMiss, String(input.failOnCacheMiss));
|
|
||||||
input.saveOnAnyFailure &&
|
|
||||||
setInput(Inputs.SaveOnAnyFailure, String(input.saveOnAnyFailure));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearInputs(): void {
|
export function clearInputs(): void {
|
||||||
delete process.env[getInputName(Inputs.Path)];
|
delete process.env[getInputName(Inputs.Path)];
|
||||||
delete process.env[getInputName(Inputs.Key)];
|
delete process.env[getInputName(Inputs.Key)];
|
||||||
delete process.env[getInputName(Inputs.RestoreKeys)];
|
delete process.env[getInputName(Inputs.RestoreKeys)];
|
||||||
delete process.env[getInputName(Inputs.FailOnCacheMiss)];
|
|
||||||
delete process.env[getInputName(Inputs.SaveOnAnyFailure)];
|
|
||||||
delete process.env[getInputName(Inputs.UploadChunkSize)];
|
delete process.env[getInputName(Inputs.UploadChunkSize)];
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue