mirror of
https://github.com/actions/cache.git
synced 2025-04-23 22:40:48 +00:00
save state
This commit is contained in:
parent
ab5323b726
commit
0ad6d2d6f2
13 changed files with 595 additions and 1301 deletions
|
@ -1 +0,0 @@
|
||||||
hello world
|
|
|
@ -1,234 +0,0 @@
|
||||||
import * as core from "@actions/core";
|
|
||||||
|
|
||||||
import { Events, Outputs, RefKey, State } from "../src/constants";
|
|
||||||
import * as actionUtils from "../src/utils/actionUtils";
|
|
||||||
import * as testUtils from "../src/utils/testUtils";
|
|
||||||
|
|
||||||
jest.mock("@actions/core");
|
|
||||||
|
|
||||||
beforeAll(() => {
|
|
||||||
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
|
|
||||||
return jest.requireActual("@actions/core").getInput(name, options);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
delete process.env[Events.Key];
|
|
||||||
delete process.env[RefKey];
|
|
||||||
});
|
|
||||||
|
|
||||||
test("isGhes returns true if server url is not github.com", () => {
|
|
||||||
try {
|
|
||||||
process.env["GITHUB_SERVER_URL"] = "http://example.com";
|
|
||||||
expect(actionUtils.isGhes()).toBe(true);
|
|
||||||
} finally {
|
|
||||||
process.env["GITHUB_SERVER_URL"] = undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("isGhes returns true when server url is github.com", () => {
|
|
||||||
try {
|
|
||||||
process.env["GITHUB_SERVER_URL"] = "http://github.com";
|
|
||||||
expect(actionUtils.isGhes()).toBe(false);
|
|
||||||
} finally {
|
|
||||||
process.env["GITHUB_SERVER_URL"] = undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test("isExactKeyMatch with undefined cache key returns false", () => {
|
|
||||||
const key = "linux-rust";
|
|
||||||
const cacheKey = undefined;
|
|
||||||
|
|
||||||
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("isExactKeyMatch with empty cache key returns false", () => {
|
|
||||||
const key = "linux-rust";
|
|
||||||
const cacheKey = "";
|
|
||||||
|
|
||||||
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("isExactKeyMatch with different keys returns false", () => {
|
|
||||||
const key = "linux-rust";
|
|
||||||
const cacheKey = "linux-";
|
|
||||||
|
|
||||||
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("isExactKeyMatch with different key accents returns false", () => {
|
|
||||||
const key = "linux-áccent";
|
|
||||||
const cacheKey = "linux-accent";
|
|
||||||
|
|
||||||
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("isExactKeyMatch with same key returns true", () => {
|
|
||||||
const key = "linux-rust";
|
|
||||||
const cacheKey = "linux-rust";
|
|
||||||
|
|
||||||
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("isExactKeyMatch with same key and different casing returns true", () => {
|
|
||||||
const key = "linux-rust";
|
|
||||||
const cacheKey = "LINUX-RUST";
|
|
||||||
|
|
||||||
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", () => {
|
|
||||||
const message = "A warning occurred.";
|
|
||||||
|
|
||||||
const infoMock = jest.spyOn(core, "info");
|
|
||||||
|
|
||||||
actionUtils.logWarning(message);
|
|
||||||
|
|
||||||
expect(infoMock).toHaveBeenCalledWith(`[warning]${message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("isValidEvent returns false for event that does not have a branch or tag", () => {
|
|
||||||
const event = "foo";
|
|
||||||
process.env[Events.Key] = event;
|
|
||||||
|
|
||||||
const isValidEvent = actionUtils.isValidEvent();
|
|
||||||
|
|
||||||
expect(isValidEvent).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("isValidEvent returns true for event that has a ref", () => {
|
|
||||||
const event = Events.Push;
|
|
||||||
process.env[Events.Key] = event;
|
|
||||||
process.env[RefKey] = "ref/heads/feature";
|
|
||||||
|
|
||||||
const isValidEvent = actionUtils.isValidEvent();
|
|
||||||
|
|
||||||
expect(isValidEvent).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getInputAsArray returns empty array if not required and missing", () => {
|
|
||||||
expect(actionUtils.getInputAsArray("foo")).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getInputAsArray throws error if required and missing", () => {
|
|
||||||
expect(() =>
|
|
||||||
actionUtils.getInputAsArray("foo", { required: true })
|
|
||||||
).toThrowError();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getInputAsArray handles single line correctly", () => {
|
|
||||||
testUtils.setInput("foo", "bar");
|
|
||||||
expect(actionUtils.getInputAsArray("foo")).toEqual(["bar"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getInputAsArray handles multiple lines correctly", () => {
|
|
||||||
testUtils.setInput("foo", "bar\nbaz");
|
|
||||||
expect(actionUtils.getInputAsArray("foo")).toEqual(["bar", "baz"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getInputAsArray handles different new lines correctly", () => {
|
|
||||||
testUtils.setInput("foo", "bar\r\nbaz");
|
|
||||||
expect(actionUtils.getInputAsArray("foo")).toEqual(["bar", "baz"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getInputAsArray handles empty lines correctly", () => {
|
|
||||||
testUtils.setInput("foo", "\n\nbar\n\nbaz\n\n");
|
|
||||||
expect(actionUtils.getInputAsArray("foo")).toEqual(["bar", "baz"]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getInputAsInt returns undefined if input not set", () => {
|
|
||||||
expect(actionUtils.getInputAsInt("undefined")).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getInputAsInt returns value if input is valid", () => {
|
|
||||||
testUtils.setInput("foo", "8");
|
|
||||||
expect(actionUtils.getInputAsInt("foo")).toBe(8);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getInputAsInt returns undefined if input is invalid or NaN", () => {
|
|
||||||
testUtils.setInput("foo", "bar");
|
|
||||||
expect(actionUtils.getInputAsInt("foo")).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getInputAsInt throws if required and value missing", () => {
|
|
||||||
expect(() =>
|
|
||||||
actionUtils.getInputAsInt("undefined", { required: true })
|
|
||||||
).toThrowError();
|
|
||||||
});
|
|
|
@ -1,17 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# Validate args
|
|
||||||
prefix="$1"
|
|
||||||
if [ -z "$prefix" ]; then
|
|
||||||
echo "Must supply prefix argument"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
path="$2"
|
|
||||||
if [ -z "$path" ]; then
|
|
||||||
echo "Must supply path argument"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p $path
|
|
||||||
echo "$prefix $GITHUB_RUN_ID" > $path/test-file.txt
|
|
|
@ -1,310 +0,0 @@
|
||||||
import * as cache from "@actions/cache";
|
|
||||||
import * as core from "@actions/core";
|
|
||||||
|
|
||||||
import { Events, Inputs, RefKey } from "../src/constants";
|
|
||||||
import run from "../src/restore";
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
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 on GHES should no-op", async () => {
|
|
||||||
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
|
|
||||||
|
|
||||||
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
|
|
||||||
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);
|
|
||||||
expect(logWarningMock).toHaveBeenCalledWith(
|
|
||||||
"Cache action is not supported on GHES. See https://github.com/actions/cache/issues/505 for more details"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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 () => {
|
|
||||||
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();
|
|
||||||
|
|
||||||
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 server error should fail", async () => {
|
|
||||||
const path = "node_modules";
|
|
||||||
const key = "node-test";
|
|
||||||
testUtils.setInputs({
|
|
||||||
path: path,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
|
|
||||||
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
|
|
||||||
const failedMock = jest.spyOn(core, "setFailed");
|
|
||||||
const stateMock = jest.spyOn(core, "saveState");
|
|
||||||
const restoreCacheMock = jest
|
|
||||||
.spyOn(cache, "restoreCache")
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
throw new Error("HTTP Error Occurred");
|
|
||||||
});
|
|
||||||
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
|
||||||
|
|
||||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
|
||||||
|
|
||||||
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
|
|
||||||
|
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
|
||||||
expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
|
|
||||||
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
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(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 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(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(infoMock).toHaveBeenCalledWith(
|
|
||||||
`Cache restored from key: ${restoreKey}`
|
|
||||||
);
|
|
||||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
|
||||||
});
|
|
|
@ -1,340 +0,0 @@
|
||||||
import * as cache from "@actions/cache";
|
|
||||||
import * as core from "@actions/core";
|
|
||||||
|
|
||||||
import { Events, Inputs, RefKey } from "../src/constants";
|
|
||||||
import run from "../src/save";
|
|
||||||
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, "getCacheState").mockImplementation(() => {
|
|
||||||
return jest.requireActual("../src/utils/actionUtils").getCacheState();
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
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();
|
|
||||||
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 on GHES should no-op", async () => {
|
|
||||||
jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true);
|
|
||||||
|
|
||||||
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
|
|
||||||
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
|
||||||
|
|
||||||
await run();
|
|
||||||
|
|
||||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
|
||||||
expect(logWarningMock).toHaveBeenCalledWith(
|
|
||||||
"Cache action is not supported on GHES. See https://github.com/actions/cache/issues/505 for more details"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
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 infoMock = jest.spyOn(core, "info");
|
|
||||||
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(infoMock).toHaveBeenCalledWith(
|
|
||||||
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
|
|
||||||
);
|
|
||||||
expect(logWarningMock).toHaveBeenCalledTimes(0);
|
|
||||||
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 () => {
|
|
||||||
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);
|
|
||||||
});
|
|
|
@ -1,36 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
|
|
||||||
# Validate args
|
|
||||||
prefix="$1"
|
|
||||||
if [ -z "$prefix" ]; then
|
|
||||||
echo "Must supply prefix argument"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
path="$2"
|
|
||||||
if [ -z "$path" ]; then
|
|
||||||
echo "Must specify path argument"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Sanity check GITHUB_RUN_ID defined
|
|
||||||
if [ -z "$GITHUB_RUN_ID" ]; then
|
|
||||||
echo "GITHUB_RUN_ID not defined"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verify file exists
|
|
||||||
file="$path/test-file.txt"
|
|
||||||
echo "Checking for $file"
|
|
||||||
if [ ! -e $file ]; then
|
|
||||||
echo "File does not exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verify file content
|
|
||||||
content="$(cat $file)"
|
|
||||||
echo "File content:\n$content"
|
|
||||||
if [ -z "$(echo $content | grep --fixed-strings "$prefix $GITHUB_RUN_ID")" ]; then
|
|
||||||
echo "Unexpected file content"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
747
package-lock.json
generated
747
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -26,7 +26,9 @@
|
||||||
"@actions/cache": "^1.0.7",
|
"@actions/cache": "^1.0.7",
|
||||||
"@actions/core": "^1.2.6",
|
"@actions/core": "^1.2.6",
|
||||||
"@actions/exec": "^1.0.1",
|
"@actions/exec": "^1.0.1",
|
||||||
"@actions/io": "^1.1.0"
|
"@actions/io": "^1.1.0",
|
||||||
|
"aws-sdk": "^2.998.0",
|
||||||
|
"filesize": "^8.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^24.0.13",
|
"@types/jest": "^24.0.13",
|
||||||
|
|
95
src/cache.service.ts
Normal file
95
src/cache.service.ts
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import * as utils from "@actions/cache/lib/internal/cacheUtils";
|
||||||
|
import { createTar, listTar } from "@actions/cache/lib/internal/tar";
|
||||||
|
import * as core from "@actions/core";
|
||||||
|
import { S3 } from "aws-sdk";
|
||||||
|
import filesize from "filesize";
|
||||||
|
import fs from "fs";
|
||||||
|
import * as path from "path";
|
||||||
|
|
||||||
|
export class CacheService {
|
||||||
|
private _client: S3;
|
||||||
|
private _bucket: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
accessKeyId: string,
|
||||||
|
secretAccessKey: string,
|
||||||
|
region: string,
|
||||||
|
bucket: string
|
||||||
|
) {
|
||||||
|
this._client = new S3({
|
||||||
|
region: region,
|
||||||
|
credentials: {
|
||||||
|
accessKeyId: accessKeyId,
|
||||||
|
secretAccessKey: secretAccessKey
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this._bucket = bucket;
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreCache(
|
||||||
|
paths: string[],
|
||||||
|
primaryKey: string,
|
||||||
|
restoreKeys: string[]
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveCache(paths: string[], key: string): Promise<string> {
|
||||||
|
const compressionMethod = await utils.getCompressionMethod();
|
||||||
|
|
||||||
|
const cachePaths = await utils.resolvePaths(paths);
|
||||||
|
core.debug("Cache Paths:");
|
||||||
|
core.debug(`${JSON.stringify(cachePaths)}`);
|
||||||
|
|
||||||
|
const archiveFolder = await utils.createTempDirectory();
|
||||||
|
const archivePath = path.join(
|
||||||
|
archiveFolder,
|
||||||
|
utils.getCacheFileName(compressionMethod)
|
||||||
|
);
|
||||||
|
|
||||||
|
core.debug(`Archive Path: ${archivePath}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await createTar(archiveFolder, cachePaths, compressionMethod);
|
||||||
|
if (core.isDebug()) {
|
||||||
|
await listTar(archivePath, compressionMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
core.info(
|
||||||
|
`Archive Size: ${filesize(fs.statSync(archivePath).size)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
core.debug(`Saving Cache (ID: ${key})`);
|
||||||
|
await this.uploadToS3(key, archivePath);
|
||||||
|
} finally {
|
||||||
|
// Try to delete the archive to save space
|
||||||
|
try {
|
||||||
|
await utils.unlinkFile(archivePath);
|
||||||
|
} catch (error) {
|
||||||
|
core.debug(`Failed to delete archive: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async uploadToS3(key: string, archivePath: string): Promise<void> {
|
||||||
|
const client = new S3();
|
||||||
|
// Read in the file, convert it to base64, store to S3
|
||||||
|
fs.readFile(archivePath, (err, data) => {
|
||||||
|
if (err) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const base64data = data.toString("base64");
|
||||||
|
|
||||||
|
client
|
||||||
|
.putObject({
|
||||||
|
Bucket: this._bucket,
|
||||||
|
Key: key,
|
||||||
|
Body: base64data
|
||||||
|
})
|
||||||
|
.send();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,11 @@ export enum Inputs {
|
||||||
Key = "key",
|
Key = "key",
|
||||||
Path = "path",
|
Path = "path",
|
||||||
RestoreKeys = "restore-keys",
|
RestoreKeys = "restore-keys",
|
||||||
UploadChunkSize = "upload-chunk-size"
|
UploadChunkSize = "upload-chunk-size",
|
||||||
|
Region = "region",
|
||||||
|
AccessKeyId = "access-key-id",
|
||||||
|
SecretAccessKey = "secret-access-key",
|
||||||
|
Bucket = "bucket"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum Outputs {
|
export enum Outputs {
|
||||||
|
|
|
@ -1,29 +1,11 @@
|
||||||
import * as cache from "@actions/cache";
|
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
import { Events, Inputs, State } from "./constants";
|
import { CacheService } from "./cache.service";
|
||||||
|
import { Inputs, State } from "./constants";
|
||||||
import * as utils from "./utils/actionUtils";
|
import * as utils from "./utils/actionUtils";
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (utils.isGhes()) {
|
|
||||||
utils.logWarning(
|
|
||||||
"Cache action is not supported on GHES. See https://github.com/actions/cache/issues/505 for more details"
|
|
||||||
);
|
|
||||||
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 });
|
const primaryKey = core.getInput(Inputs.Key, { required: true });
|
||||||
core.saveState(State.CachePrimaryKey, primaryKey);
|
core.saveState(State.CachePrimaryKey, primaryKey);
|
||||||
|
|
||||||
|
@ -33,6 +15,13 @@ async function run(): Promise<void> {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const cache: CacheService = new CacheService(
|
||||||
|
core.getInput(Inputs.AccessKeyId),
|
||||||
|
core.getInput(Inputs.SecretAccessKey),
|
||||||
|
core.getInput(Inputs.Region),
|
||||||
|
core.getInput(Inputs.Bucket)
|
||||||
|
);
|
||||||
|
|
||||||
const cacheKey = await cache.restoreCache(
|
const cacheKey = await cache.restoreCache(
|
||||||
cachePaths,
|
cachePaths,
|
||||||
primaryKey,
|
primaryKey,
|
||||||
|
@ -56,13 +45,9 @@ async function run(): Promise<void> {
|
||||||
|
|
||||||
core.info(`Cache restored from key: ${cacheKey}`);
|
core.info(`Cache restored from key: ${cacheKey}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === cache.ValidationError.name) {
|
|
||||||
throw error;
|
|
||||||
} else {
|
|
||||||
utils.logWarning(error.message);
|
utils.logWarning(error.message);
|
||||||
utils.setCacheHitOutput(false);
|
utils.setCacheHitOutput(false);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
core.setFailed(error.message);
|
core.setFailed(error.message);
|
||||||
}
|
}
|
||||||
|
|
37
src/save.ts
37
src/save.ts
|
@ -1,7 +1,8 @@
|
||||||
import * as cache from "@actions/cache";
|
import {} from "@actions/cache";
|
||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
|
|
||||||
import { Events, Inputs, State } from "./constants";
|
import { CacheService } from "./cache.service";
|
||||||
|
import { Inputs, State } from "./constants";
|
||||||
import * as utils from "./utils/actionUtils";
|
import * as utils from "./utils/actionUtils";
|
||||||
|
|
||||||
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
|
// Catch and log any unhandled exceptions. These exceptions can leak out of the uploadChunk method in
|
||||||
|
@ -11,22 +12,6 @@ process.on("uncaughtException", e => utils.logWarning(e.message));
|
||||||
|
|
||||||
async function run(): Promise<void> {
|
async function run(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
if (utils.isGhes()) {
|
|
||||||
utils.logWarning(
|
|
||||||
"Cache action is not supported on GHES. See https://github.com/actions/cache/issues/505 for more details"
|
|
||||||
);
|
|
||||||
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();
|
const state = utils.getCacheState();
|
||||||
|
|
||||||
// Inputs are re-evaluted before the post action, so we want the original key used for restore
|
// Inputs are re-evaluted before the post action, so we want the original key used for restore
|
||||||
|
@ -48,19 +33,17 @@ async function run(): Promise<void> {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await cache.saveCache(cachePaths, primaryKey, {
|
const cache: CacheService = new CacheService(
|
||||||
uploadChunkSize: utils.getInputAsInt(Inputs.UploadChunkSize)
|
core.getInput(Inputs.AccessKeyId),
|
||||||
});
|
core.getInput(Inputs.SecretAccessKey),
|
||||||
|
core.getInput(Inputs.Region),
|
||||||
|
core.getInput(Inputs.Bucket)
|
||||||
|
);
|
||||||
|
await cache.saveCache(cachePaths, primaryKey);
|
||||||
core.info(`Cache saved with key: ${primaryKey}`);
|
core.info(`Cache saved with key: ${primaryKey}`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === cache.ValidationError.name) {
|
|
||||||
throw error;
|
|
||||||
} else if (error.name === cache.ReserveCacheError.name) {
|
|
||||||
core.info(error.message);
|
|
||||||
} else {
|
|
||||||
utils.logWarning(error.message);
|
utils.logWarning(error.message);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
utils.logWarning(error.message);
|
utils.logWarning(error.message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
import { Inputs } from "../constants";
|
|
||||||
|
|
||||||
// See: https://github.com/actions/toolkit/blob/master/packages/core/src/core.ts#L67
|
|
||||||
function getInputName(name: string): string {
|
|
||||||
return `INPUT_${name.replace(/ /g, "_").toUpperCase()}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setInput(name: string, value: string): void {
|
|
||||||
process.env[getInputName(name)] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CacheInput {
|
|
||||||
path: string;
|
|
||||||
key: string;
|
|
||||||
restoreKeys?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setInputs(input: CacheInput): void {
|
|
||||||
setInput(Inputs.Path, input.path);
|
|
||||||
setInput(Inputs.Key, input.key);
|
|
||||||
input.restoreKeys &&
|
|
||||||
setInput(Inputs.RestoreKeys, input.restoreKeys.join("\n"));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearInputs(): void {
|
|
||||||
delete process.env[getInputName(Inputs.Path)];
|
|
||||||
delete process.env[getInputName(Inputs.Key)];
|
|
||||||
delete process.env[getInputName(Inputs.RestoreKeys)];
|
|
||||||
delete process.env[getInputName(Inputs.UploadChunkSize)];
|
|
||||||
}
|
|
Loading…
Add table
Reference in a new issue