From 77fd223211c24ba18a952bc90d8762ec859fb8f6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Andreas=20M=C3=B6ller?= <am@localheinz.com>
Date: Sat, 16 May 2020 23:05:56 +0200
Subject: [PATCH] Enhancement: Allow usage when GITHUB_REF or ACTIONS_CACHE_REF
 are defined

---
 __tests__/actionUtils.test.ts |  28 +-
 __tests__/restore.test.ts     | 636 +++++++++++++++++-------------
 __tests__/save.test.ts        | 720 ++++++++++++++++++----------------
 dist/restore/index.js         |  15 +-
 dist/save/index.js            |  15 +-
 src/constants.ts              |   2 +-
 src/utils/actionUtils.ts      |  14 +-
 7 files changed, 792 insertions(+), 638 deletions(-)

diff --git a/__tests__/actionUtils.test.ts b/__tests__/actionUtils.test.ts
index d6f9f88..dda0310 100644
--- a/__tests__/actionUtils.test.ts
+++ b/__tests__/actionUtils.test.ts
@@ -4,7 +4,7 @@ import { promises as fs } from "fs";
 import * as os from "os";
 import * as path from "path";
 
-import { Events, Outputs, RefKey, State } from "../src/constants";
+import { Events, Outputs, RefKeys, State } from "../src/constants";
 import { ArtifactCacheEntry } from "../src/contracts";
 import * as actionUtils from "../src/utils/actionUtils";
 
@@ -19,7 +19,8 @@ function getTempDir(): string {
 
 afterEach(() => {
     delete process.env[Events.Key];
-    delete process.env[RefKey];
+
+    RefKeys.forEach(refKey => delete process.env[refKey]);
 });
 
 afterAll(async () => {
@@ -326,16 +327,23 @@ test("resolvePaths exclusion pattern returns not found", async () => {
     }
 });
 
-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);
+const refKeySet = RefKeys.map(refKey => {
+    return [refKey];
 });
 
+test.each(refKeySet)(
+    "isValidEvent returns true for event that has a ref",
+    refKey => {
+        const event = Events.Push;
+        process.env[Events.Key] = event;
+        process.env[refKey] = "ref/heads/feature";
+
+        const isValidEvent = actionUtils.isValidEvent();
+
+        expect(isValidEvent).toBe(true);
+    }
+);
+
 test("unlinkFile unlinks file", async () => {
     const testDirectory = await fs.mkdtemp("unlinkFileTest");
     const testFile = path.join(testDirectory, "test.txt");
diff --git a/__tests__/restore.test.ts b/__tests__/restore.test.ts
index ff217ac..16b9137 100644
--- a/__tests__/restore.test.ts
+++ b/__tests__/restore.test.ts
@@ -7,7 +7,7 @@ import {
     CompressionMethod,
     Events,
     Inputs,
-    RefKey
+    RefKeys
 } from "../src/constants";
 import { ArtifactCacheEntry } from "../src/contracts";
 import run from "../src/restore";
@@ -40,13 +40,17 @@ beforeAll(() => {
 
 beforeEach(() => {
     process.env[Events.Key] = Events.Push;
-    process.env[RefKey] = "refs/heads/feature-branch";
 });
 
 afterEach(() => {
     testUtils.clearInputs();
     delete process.env[Events.Key];
-    delete process.env[RefKey];
+
+    RefKeys.forEach(refKey => delete process.env[refKey]);
+});
+
+const refKeySet = RefKeys.map(refKey => {
+    return [refKey, `refs/heads/feature/${refKey.toLowerCase()}`];
 });
 
 test("restore with invalid event outputs warning", async () => {
@@ -54,7 +58,6 @@ test("restore with invalid event outputs warning", async () => {
     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.`
@@ -62,16 +65,23 @@ test("restore with invalid event outputs warning", async () => {
     expect(failedMock).toHaveBeenCalledTimes(0);
 });
 
-test("restore with no path should fail", async () => {
-    const failedMock = jest.spyOn(core, "setFailed");
-    await run();
-    // 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.each(refKeySet)(
+    "restore with no path should fail",
+    async (refKey, ref) => {
+        process.env[refKey] = ref;
+
+        const failedMock = jest.spyOn(core, "setFailed");
+        await run();
+        // 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.each(refKeySet)("restore with no key", async (refKey, ref) => {
+    process.env[refKey] = ref;
 
-test("restore with no key", async () => {
     testUtils.setInput(Inputs.Path, "node_modules");
     const failedMock = jest.spyOn(core, "setFailed");
     await run();
@@ -80,48 +90,65 @@ test("restore with no key", async () => {
     );
 });
 
-test("restore with too many keys should fail", async () => {
-    const key = "node-test";
-    const restoreKeys = [...Array(20).keys()].map(x => x.toString());
-    testUtils.setInputs({
-        path: "node_modules",
-        key,
-        restoreKeys
-    });
-    const failedMock = jest.spyOn(core, "setFailed");
-    await run();
-    expect(failedMock).toHaveBeenCalledWith(
-        `Key Validation Error: Keys are limited to a maximum of 10.`
-    );
-});
+test.each(refKeySet)(
+    "restore with too many keys should fail",
+    async (refKey, ref) => {
+        process.env[refKey] = ref;
 
-test("restore with large key should fail", async () => {
-    const key = "foo".repeat(512); // Over the 512 character limit
-    testUtils.setInputs({
-        path: "node_modules",
-        key
-    });
-    const failedMock = jest.spyOn(core, "setFailed");
-    await run();
-    expect(failedMock).toHaveBeenCalledWith(
-        `Key Validation Error: ${key} cannot be larger than 512 characters.`
-    );
-});
+        const key = "node-test";
+        const restoreKeys = [...Array(20).keys()].map(x => x.toString());
+        testUtils.setInputs({
+            path: "node_modules",
+            key,
+            restoreKeys
+        });
+        const failedMock = jest.spyOn(core, "setFailed");
+        await run();
+        expect(failedMock).toHaveBeenCalledWith(
+            `Key Validation Error: Keys are limited to a maximum of 10.`
+        );
+    }
+);
 
-test("restore with invalid key should fail", async () => {
-    const key = "comma,comma";
-    testUtils.setInputs({
-        path: "node_modules",
-        key
-    });
-    const failedMock = jest.spyOn(core, "setFailed");
-    await run();
-    expect(failedMock).toHaveBeenCalledWith(
-        `Key Validation Error: ${key} cannot contain commas.`
-    );
-});
+test.each(refKeySet)(
+    "restore with large key should fail",
+    async (refKey, ref) => {
+        process.env[refKey] = ref;
+
+        const key = "foo".repeat(512); // Over the 512 character limit
+        testUtils.setInputs({
+            path: "node_modules",
+            key
+        });
+        const failedMock = jest.spyOn(core, "setFailed");
+        await run();
+        expect(failedMock).toHaveBeenCalledWith(
+            `Key Validation Error: ${key} cannot be larger than 512 characters.`
+        );
+    }
+);
+
+test.each(refKeySet)(
+    "restore with invalid key should fail",
+    async (refKey, ref) => {
+        process.env[refKey] = ref;
+
+        const key = "comma,comma";
+        testUtils.setInputs({
+            path: "node_modules",
+            key
+        });
+        const failedMock = jest.spyOn(core, "setFailed");
+        await run();
+        expect(failedMock).toHaveBeenCalledWith(
+            `Key Validation Error: ${key} cannot contain commas.`
+        );
+    }
+);
+
+test.each(refKeySet)("restore with no cache found", async (refKey, ref) => {
+    process.env[refKey] = ref;
 
-test("restore with no cache found", async () => {
     const key = "node-test";
     testUtils.setInputs({
         path: "node_modules",
@@ -147,287 +174,330 @@ test("restore with no cache found", async () => {
     );
 });
 
-test("restore with server error should fail", async () => {
-    const key = "node-test";
-    testUtils.setInputs({
-        path: "node_modules",
-        key
-    });
+test.each(refKeySet)(
+    "restore with server error should fail",
+    async (refKey, ref) => {
+        process.env[refKey] = ref;
 
-    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
-    const failedMock = jest.spyOn(core, "setFailed");
-    const stateMock = jest.spyOn(core, "saveState");
+        const key = "node-test";
+        testUtils.setInputs({
+            path: "node_modules",
+            key
+        });
 
-    const clientMock = jest.spyOn(cacheHttpClient, "getCacheEntry");
-    clientMock.mockImplementation(() => {
-        throw new Error("HTTP Error Occurred");
-    });
+        const logWarningMock = jest.spyOn(actionUtils, "logWarning");
+        const failedMock = jest.spyOn(core, "setFailed");
+        const stateMock = jest.spyOn(core, "saveState");
 
-    const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
+        const clientMock = jest.spyOn(cacheHttpClient, "getCacheEntry");
+        clientMock.mockImplementation(() => {
+            throw new Error("HTTP Error Occurred");
+        });
 
-    await run();
+        const setCacheHitOutputMock = jest.spyOn(
+            actionUtils,
+            "setCacheHitOutput"
+        );
 
-    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
+        await run();
 
-    expect(logWarningMock).toHaveBeenCalledTimes(1);
-    expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
+        expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
 
-    expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
-    expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
+        expect(logWarningMock).toHaveBeenCalledTimes(1);
+        expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
 
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
+        expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
+        expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
 
-test("restore with restore keys and no cache found", async () => {
-    const key = "node-test";
-    const restoreKey = "node-";
-    testUtils.setInputs({
-        path: "node_modules",
-        key,
-        restoreKeys: [restoreKey]
-    });
+        expect(failedMock).toHaveBeenCalledTimes(0);
+    }
+);
 
-    const infoMock = jest.spyOn(core, "info");
-    const failedMock = jest.spyOn(core, "setFailed");
-    const stateMock = jest.spyOn(core, "saveState");
+test.each(refKeySet)(
+    "restore with restore keys and no cache found",
+    async (refKey, ref) => {
+        process.env[refKey] = ref;
 
-    const clientMock = jest.spyOn(cacheHttpClient, "getCacheEntry");
-    clientMock.mockImplementation(() => {
-        return Promise.resolve(null);
-    });
+        const key = "node-test";
+        const restoreKey = "node-";
+        testUtils.setInputs({
+            path: "node_modules",
+            key,
+            restoreKeys: [restoreKey]
+        });
 
-    await run();
+        const infoMock = jest.spyOn(core, "info");
+        const failedMock = jest.spyOn(core, "setFailed");
+        const stateMock = jest.spyOn(core, "saveState");
 
-    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
-    expect(failedMock).toHaveBeenCalledTimes(0);
+        const clientMock = jest.spyOn(cacheHttpClient, "getCacheEntry");
+        clientMock.mockImplementation(() => {
+            return Promise.resolve(null);
+        });
 
-    expect(infoMock).toHaveBeenCalledWith(
-        `Cache not found for input keys: ${key}, ${restoreKey}`
-    );
-});
+        await run();
 
-test("restore with gzip compressed cache found", async () => {
-    const key = "node-test";
-    testUtils.setInputs({
-        path: "node_modules",
-        key
-    });
+        expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
+        expect(failedMock).toHaveBeenCalledTimes(0);
 
-    const infoMock = jest.spyOn(core, "info");
-    const failedMock = jest.spyOn(core, "setFailed");
-    const stateMock = jest.spyOn(core, "saveState");
+        expect(infoMock).toHaveBeenCalledWith(
+            `Cache not found for input keys: ${key}, ${restoreKey}`
+        );
+    }
+);
 
-    const cacheEntry: ArtifactCacheEntry = {
-        cacheKey: key,
-        scope: "refs/heads/master",
-        archiveLocation: "www.actionscache.test/download"
-    };
-    const getCacheMock = jest.spyOn(cacheHttpClient, "getCacheEntry");
-    getCacheMock.mockImplementation(() => {
-        return Promise.resolve(cacheEntry);
-    });
-    const tempPath = "/foo/bar";
+test.each(refKeySet)(
+    "restore with gzip compressed cache found",
+    async (refKey, ref) => {
+        process.env[refKey] = ref;
 
-    const createTempDirectoryMock = jest.spyOn(
-        actionUtils,
-        "createTempDirectory"
-    );
-    createTempDirectoryMock.mockImplementation(() => {
-        return Promise.resolve(tempPath);
-    });
+        const key = "node-test";
+        testUtils.setInputs({
+            path: "node_modules",
+            key
+        });
 
-    const archivePath = path.join(tempPath, CacheFilename.Gzip);
-    const setCacheStateMock = jest.spyOn(actionUtils, "setCacheState");
-    const downloadCacheMock = jest.spyOn(cacheHttpClient, "downloadCache");
+        const infoMock = jest.spyOn(core, "info");
+        const failedMock = jest.spyOn(core, "setFailed");
+        const stateMock = jest.spyOn(core, "saveState");
 
-    const fileSize = 142;
-    const getArchiveFileSizeMock = jest
-        .spyOn(actionUtils, "getArchiveFileSize")
-        .mockReturnValue(fileSize);
+        const cacheEntry: ArtifactCacheEntry = {
+            cacheKey: key,
+            scope: "refs/heads/master",
+            archiveLocation: "www.actionscache.test/download"
+        };
+        const getCacheMock = jest.spyOn(cacheHttpClient, "getCacheEntry");
+        getCacheMock.mockImplementation(() => {
+            return Promise.resolve(cacheEntry);
+        });
+        const tempPath = "/foo/bar";
 
-    const extractTarMock = jest.spyOn(tar, "extractTar");
-    const unlinkFileMock = jest.spyOn(actionUtils, "unlinkFile");
-    const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
+        const createTempDirectoryMock = jest.spyOn(
+            actionUtils,
+            "createTempDirectory"
+        );
+        createTempDirectoryMock.mockImplementation(() => {
+            return Promise.resolve(tempPath);
+        });
 
-    const compression = CompressionMethod.Gzip;
-    const getCompressionMock = jest
-        .spyOn(actionUtils, "getCompressionMethod")
-        .mockReturnValue(Promise.resolve(compression));
+        const archivePath = path.join(tempPath, CacheFilename.Gzip);
+        const setCacheStateMock = jest.spyOn(actionUtils, "setCacheState");
+        const downloadCacheMock = jest.spyOn(cacheHttpClient, "downloadCache");
 
-    await run();
+        const fileSize = 142;
+        const getArchiveFileSizeMock = jest
+            .spyOn(actionUtils, "getArchiveFileSize")
+            .mockReturnValue(fileSize);
 
-    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
-    expect(getCacheMock).toHaveBeenCalledWith([key], {
-        compressionMethod: compression
-    });
-    expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry);
-    expect(createTempDirectoryMock).toHaveBeenCalledTimes(1);
-    expect(downloadCacheMock).toHaveBeenCalledWith(
-        cacheEntry.archiveLocation,
-        archivePath
-    );
-    expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath);
+        const extractTarMock = jest.spyOn(tar, "extractTar");
+        const unlinkFileMock = jest.spyOn(actionUtils, "unlinkFile");
+        const setCacheHitOutputMock = jest.spyOn(
+            actionUtils,
+            "setCacheHitOutput"
+        );
 
-    expect(extractTarMock).toHaveBeenCalledTimes(1);
-    expect(extractTarMock).toHaveBeenCalledWith(archivePath, compression);
+        const compression = CompressionMethod.Gzip;
+        const getCompressionMock = jest
+            .spyOn(actionUtils, "getCompressionMethod")
+            .mockReturnValue(Promise.resolve(compression));
 
-    expect(unlinkFileMock).toHaveBeenCalledTimes(1);
-    expect(unlinkFileMock).toHaveBeenCalledWith(archivePath);
+        await run();
 
-    expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
-    expect(setCacheHitOutputMock).toHaveBeenCalledWith(true);
+        expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
+        expect(getCacheMock).toHaveBeenCalledWith([key], {
+            compressionMethod: compression
+        });
+        expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry);
+        expect(createTempDirectoryMock).toHaveBeenCalledTimes(1);
+        expect(downloadCacheMock).toHaveBeenCalledWith(
+            cacheEntry.archiveLocation,
+            archivePath
+        );
+        expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath);
 
-    expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
-    expect(failedMock).toHaveBeenCalledTimes(0);
-    expect(getCompressionMock).toHaveBeenCalledTimes(1);
-});
+        expect(extractTarMock).toHaveBeenCalledTimes(1);
+        expect(extractTarMock).toHaveBeenCalledWith(archivePath, compression);
 
-test("restore with a pull request event and zstd compressed cache found", async () => {
-    const key = "node-test";
-    testUtils.setInputs({
-        path: "node_modules",
-        key
-    });
+        expect(unlinkFileMock).toHaveBeenCalledTimes(1);
+        expect(unlinkFileMock).toHaveBeenCalledWith(archivePath);
 
-    process.env[Events.Key] = Events.PullRequest;
+        expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
+        expect(setCacheHitOutputMock).toHaveBeenCalledWith(true);
 
-    const infoMock = jest.spyOn(core, "info");
-    const failedMock = jest.spyOn(core, "setFailed");
-    const stateMock = jest.spyOn(core, "saveState");
+        expect(infoMock).toHaveBeenCalledWith(
+            `Cache restored from key: ${key}`
+        );
+        expect(failedMock).toHaveBeenCalledTimes(0);
+        expect(getCompressionMock).toHaveBeenCalledTimes(1);
+    }
+);
 
-    const cacheEntry: ArtifactCacheEntry = {
-        cacheKey: key,
-        scope: "refs/heads/master",
-        archiveLocation: "www.actionscache.test/download"
-    };
-    const getCacheMock = jest.spyOn(cacheHttpClient, "getCacheEntry");
-    getCacheMock.mockImplementation(() => {
-        return Promise.resolve(cacheEntry);
-    });
-    const tempPath = "/foo/bar";
+test.each(refKeySet)(
+    "restore with a pull request event and zstd compressed cache found",
+    async (refKey, ref) => {
+        process.env[refKey] = ref;
 
-    const createTempDirectoryMock = jest.spyOn(
-        actionUtils,
-        "createTempDirectory"
-    );
-    createTempDirectoryMock.mockImplementation(() => {
-        return Promise.resolve(tempPath);
-    });
+        const key = "node-test";
+        testUtils.setInputs({
+            path: "node_modules",
+            key
+        });
 
-    const archivePath = path.join(tempPath, CacheFilename.Zstd);
-    const setCacheStateMock = jest.spyOn(actionUtils, "setCacheState");
-    const downloadCacheMock = jest.spyOn(cacheHttpClient, "downloadCache");
+        process.env[Events.Key] = Events.PullRequest;
 
-    const fileSize = 62915000;
-    const getArchiveFileSizeMock = jest
-        .spyOn(actionUtils, "getArchiveFileSize")
-        .mockReturnValue(fileSize);
+        const infoMock = jest.spyOn(core, "info");
+        const failedMock = jest.spyOn(core, "setFailed");
+        const stateMock = jest.spyOn(core, "saveState");
 
-    const extractTarMock = jest.spyOn(tar, "extractTar");
-    const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
-    const compression = CompressionMethod.Zstd;
-    const getCompressionMock = jest
-        .spyOn(actionUtils, "getCompressionMethod")
-        .mockReturnValue(Promise.resolve(compression));
+        const cacheEntry: ArtifactCacheEntry = {
+            cacheKey: key,
+            scope: "refs/heads/master",
+            archiveLocation: "www.actionscache.test/download"
+        };
+        const getCacheMock = jest.spyOn(cacheHttpClient, "getCacheEntry");
+        getCacheMock.mockImplementation(() => {
+            return Promise.resolve(cacheEntry);
+        });
+        const tempPath = "/foo/bar";
 
-    await run();
+        const createTempDirectoryMock = jest.spyOn(
+            actionUtils,
+            "createTempDirectory"
+        );
+        createTempDirectoryMock.mockImplementation(() => {
+            return Promise.resolve(tempPath);
+        });
 
-    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
-    expect(getCacheMock).toHaveBeenCalledWith([key], {
-        compressionMethod: compression
-    });
-    expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry);
-    expect(createTempDirectoryMock).toHaveBeenCalledTimes(1);
-    expect(downloadCacheMock).toHaveBeenCalledWith(
-        cacheEntry.archiveLocation,
-        archivePath
-    );
-    expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath);
-    expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~60 MB (62915000 B)`);
+        const archivePath = path.join(tempPath, CacheFilename.Zstd);
+        const setCacheStateMock = jest.spyOn(actionUtils, "setCacheState");
+        const downloadCacheMock = jest.spyOn(cacheHttpClient, "downloadCache");
 
-    expect(extractTarMock).toHaveBeenCalledTimes(1);
-    expect(extractTarMock).toHaveBeenCalledWith(archivePath, compression);
+        const fileSize = 62915000;
+        const getArchiveFileSizeMock = jest
+            .spyOn(actionUtils, "getArchiveFileSize")
+            .mockReturnValue(fileSize);
 
-    expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
-    expect(setCacheHitOutputMock).toHaveBeenCalledWith(true);
+        const extractTarMock = jest.spyOn(tar, "extractTar");
+        const setCacheHitOutputMock = jest.spyOn(
+            actionUtils,
+            "setCacheHitOutput"
+        );
+        const compression = CompressionMethod.Zstd;
+        const getCompressionMock = jest
+            .spyOn(actionUtils, "getCompressionMethod")
+            .mockReturnValue(Promise.resolve(compression));
 
-    expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
-    expect(failedMock).toHaveBeenCalledTimes(0);
-    expect(getCompressionMock).toHaveBeenCalledTimes(1);
-});
+        await run();
 
-test("restore with cache found for restore key", async () => {
-    const key = "node-test";
-    const restoreKey = "node-";
-    testUtils.setInputs({
-        path: "node_modules",
-        key,
-        restoreKeys: [restoreKey]
-    });
+        expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
+        expect(getCacheMock).toHaveBeenCalledWith([key], {
+            compressionMethod: compression
+        });
+        expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry);
+        expect(createTempDirectoryMock).toHaveBeenCalledTimes(1);
+        expect(downloadCacheMock).toHaveBeenCalledWith(
+            cacheEntry.archiveLocation,
+            archivePath
+        );
+        expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath);
+        expect(infoMock).toHaveBeenCalledWith(
+            `Cache Size: ~60 MB (62915000 B)`
+        );
 
-    const infoMock = jest.spyOn(core, "info");
-    const failedMock = jest.spyOn(core, "setFailed");
-    const stateMock = jest.spyOn(core, "saveState");
+        expect(extractTarMock).toHaveBeenCalledTimes(1);
+        expect(extractTarMock).toHaveBeenCalledWith(archivePath, compression);
 
-    const cacheEntry: ArtifactCacheEntry = {
-        cacheKey: restoreKey,
-        scope: "refs/heads/master",
-        archiveLocation: "www.actionscache.test/download"
-    };
-    const getCacheMock = jest.spyOn(cacheHttpClient, "getCacheEntry");
-    getCacheMock.mockImplementation(() => {
-        return Promise.resolve(cacheEntry);
-    });
-    const tempPath = "/foo/bar";
+        expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
+        expect(setCacheHitOutputMock).toHaveBeenCalledWith(true);
 
-    const createTempDirectoryMock = jest.spyOn(
-        actionUtils,
-        "createTempDirectory"
-    );
-    createTempDirectoryMock.mockImplementation(() => {
-        return Promise.resolve(tempPath);
-    });
+        expect(infoMock).toHaveBeenCalledWith(
+            `Cache restored from key: ${key}`
+        );
+        expect(failedMock).toHaveBeenCalledTimes(0);
+        expect(getCompressionMock).toHaveBeenCalledTimes(1);
+    }
+);
 
-    const archivePath = path.join(tempPath, CacheFilename.Zstd);
-    const setCacheStateMock = jest.spyOn(actionUtils, "setCacheState");
-    const downloadCacheMock = jest.spyOn(cacheHttpClient, "downloadCache");
+test.each(refKeySet)(
+    "restore with cache found for restore key",
+    async (refKey, ref) => {
+        process.env[refKey] = ref;
 
-    const fileSize = 142;
-    const getArchiveFileSizeMock = jest
-        .spyOn(actionUtils, "getArchiveFileSize")
-        .mockReturnValue(fileSize);
+        const key = "node-test";
+        const restoreKey = "node-";
+        testUtils.setInputs({
+            path: "node_modules",
+            key,
+            restoreKeys: [restoreKey]
+        });
 
-    const extractTarMock = jest.spyOn(tar, "extractTar");
-    const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
-    const compression = CompressionMethod.Zstd;
-    const getCompressionMock = jest
-        .spyOn(actionUtils, "getCompressionMethod")
-        .mockReturnValue(Promise.resolve(compression));
+        const infoMock = jest.spyOn(core, "info");
+        const failedMock = jest.spyOn(core, "setFailed");
+        const stateMock = jest.spyOn(core, "saveState");
 
-    await run();
+        const cacheEntry: ArtifactCacheEntry = {
+            cacheKey: restoreKey,
+            scope: "refs/heads/master",
+            archiveLocation: "www.actionscache.test/download"
+        };
+        const getCacheMock = jest.spyOn(cacheHttpClient, "getCacheEntry");
+        getCacheMock.mockImplementation(() => {
+            return Promise.resolve(cacheEntry);
+        });
+        const tempPath = "/foo/bar";
 
-    expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
-    expect(getCacheMock).toHaveBeenCalledWith([key, restoreKey], {
-        compressionMethod: compression
-    });
-    expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry);
-    expect(createTempDirectoryMock).toHaveBeenCalledTimes(1);
-    expect(downloadCacheMock).toHaveBeenCalledWith(
-        cacheEntry.archiveLocation,
-        archivePath
-    );
-    expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath);
-    expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~0 MB (142 B)`);
+        const createTempDirectoryMock = jest.spyOn(
+            actionUtils,
+            "createTempDirectory"
+        );
+        createTempDirectoryMock.mockImplementation(() => {
+            return Promise.resolve(tempPath);
+        });
 
-    expect(extractTarMock).toHaveBeenCalledTimes(1);
-    expect(extractTarMock).toHaveBeenCalledWith(archivePath, compression);
+        const archivePath = path.join(tempPath, CacheFilename.Zstd);
+        const setCacheStateMock = jest.spyOn(actionUtils, "setCacheState");
+        const downloadCacheMock = jest.spyOn(cacheHttpClient, "downloadCache");
 
-    expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
-    expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
+        const fileSize = 142;
+        const getArchiveFileSizeMock = jest
+            .spyOn(actionUtils, "getArchiveFileSize")
+            .mockReturnValue(fileSize);
 
-    expect(infoMock).toHaveBeenCalledWith(
-        `Cache restored from key: ${restoreKey}`
-    );
-    expect(failedMock).toHaveBeenCalledTimes(0);
-    expect(getCompressionMock).toHaveBeenCalledTimes(1);
-});
+        const extractTarMock = jest.spyOn(tar, "extractTar");
+        const setCacheHitOutputMock = jest.spyOn(
+            actionUtils,
+            "setCacheHitOutput"
+        );
+        const compression = CompressionMethod.Zstd;
+        const getCompressionMock = jest
+            .spyOn(actionUtils, "getCompressionMethod")
+            .mockReturnValue(Promise.resolve(compression));
+
+        await run();
+
+        expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
+        expect(getCacheMock).toHaveBeenCalledWith([key, restoreKey], {
+            compressionMethod: compression
+        });
+        expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry);
+        expect(createTempDirectoryMock).toHaveBeenCalledTimes(1);
+        expect(downloadCacheMock).toHaveBeenCalledWith(
+            cacheEntry.archiveLocation,
+            archivePath
+        );
+        expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath);
+        expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~0 MB (142 B)`);
+
+        expect(extractTarMock).toHaveBeenCalledTimes(1);
+        expect(extractTarMock).toHaveBeenCalledWith(archivePath, compression);
+
+        expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
+        expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
+
+        expect(infoMock).toHaveBeenCalledWith(
+            `Cache restored from key: ${restoreKey}`
+        );
+        expect(failedMock).toHaveBeenCalledTimes(0);
+        expect(getCompressionMock).toHaveBeenCalledTimes(1);
+    }
+);
diff --git a/__tests__/save.test.ts b/__tests__/save.test.ts
index 365a2fa..23a2725 100644
--- a/__tests__/save.test.ts
+++ b/__tests__/save.test.ts
@@ -7,7 +7,7 @@ import {
     CompressionMethod,
     Events,
     Inputs,
-    RefKey
+    RefKeys
 } from "../src/constants";
 import { ArtifactCacheEntry } from "../src/contracts";
 import run from "../src/save";
@@ -60,368 +60,418 @@ beforeAll(() => {
 
 beforeEach(() => {
     process.env[Events.Key] = Events.Push;
-    process.env[RefKey] = "refs/heads/feature-branch";
 });
 
 afterEach(() => {
     testUtils.clearInputs();
     delete process.env[Events.Key];
-    delete process.env[RefKey];
+
+    RefKeys.forEach(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);
+const refKeySet = RefKeys.map(refKey => {
+    return [refKey, `refs/heads/feature/${refKey.toLowerCase()}`];
 });
 
-test("save with no primary key in state outputs warning", async () => {
-    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
-    const failedMock = jest.spyOn(core, "setFailed");
+test.each(refKeySet)(
+    "save with invalid event outputs warning",
+    async refKey => {
+        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);
+    }
+);
 
-    const cacheEntry: ArtifactCacheEntry = {
-        cacheKey: "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43",
-        scope: "refs/heads/master",
-        creationTime: "2019-11-13T19:18:02+00:00",
-        archiveLocation: "www.actionscache.test/download"
-    };
+test.each(refKeySet)(
+    "save with no primary key in state outputs warning",
+    async (refKey, ref) => {
+        process.env[refKey] = ref;
 
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return JSON.stringify(cacheEntry);
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return "";
+        const logWarningMock = jest.spyOn(actionUtils, "logWarning");
+        const failedMock = jest.spyOn(core, "setFailed");
+
+        const cacheEntry: ArtifactCacheEntry = {
+            cacheKey: "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43",
+            scope: "refs/heads/master",
+            creationTime: "2019-11-13T19:18:02+00:00",
+            archiveLocation: "www.actionscache.test/download"
+        };
+
+        jest.spyOn(core, "getState")
+            // Cache Entry State
+            .mockImplementationOnce(() => {
+                return JSON.stringify(cacheEntry);
+            })
+            // Cache Key State
+            .mockImplementationOnce(() => {
+                return "";
+            });
+
+        await run();
+
+        expect(logWarningMock).toHaveBeenCalledWith(
+            `Error retrieving key from state.`
+        );
+        expect(logWarningMock).toHaveBeenCalledTimes(1);
+        expect(failedMock).toHaveBeenCalledTimes(0);
+    }
+);
+
+test.each(refKeySet)(
+    "save with exact match returns early",
+    async (refKey, ref) => {
+        process.env[refKey] = ref;
+
+        const infoMock = jest.spyOn(core, "info");
+        const failedMock = jest.spyOn(core, "setFailed");
+
+        const primaryKey =
+            "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
+        const cacheEntry: ArtifactCacheEntry = {
+            cacheKey: primaryKey,
+            scope: "refs/heads/master",
+            creationTime: "2019-11-13T19:18:02+00:00",
+            archiveLocation: "www.actionscache.test/download"
+        };
+
+        jest.spyOn(core, "getState")
+            // Cache Entry State
+            .mockImplementationOnce(() => {
+                return JSON.stringify(cacheEntry);
+            })
+            // Cache Key State
+            .mockImplementationOnce(() => {
+                return primaryKey;
+            });
+
+        const createTarMock = jest.spyOn(tar, "createTar");
+
+        await run();
+
+        expect(infoMock).toHaveBeenCalledWith(
+            `Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
+        );
+
+        expect(createTarMock).toHaveBeenCalledTimes(0);
+
+        expect(failedMock).toHaveBeenCalledTimes(0);
+    }
+);
+
+test.each(refKeySet)(
+    "save with missing input outputs warning",
+    async (refKey, ref) => {
+        process.env[refKey] = ref;
+
+        const logWarningMock = jest.spyOn(actionUtils, "logWarning");
+        const failedMock = jest.spyOn(core, "setFailed");
+
+        const primaryKey =
+            "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
+        const cacheEntry: ArtifactCacheEntry = {
+            cacheKey: "Linux-node-",
+            scope: "refs/heads/master",
+            creationTime: "2019-11-13T19:18:02+00:00",
+            archiveLocation: "www.actionscache.test/download"
+        };
+
+        jest.spyOn(core, "getState")
+            // Cache Entry State
+            .mockImplementationOnce(() => {
+                return JSON.stringify(cacheEntry);
+            })
+            // Cache Key State
+            .mockImplementationOnce(() => {
+                return primaryKey;
+            });
+
+        await run();
+
+        expect(logWarningMock).toHaveBeenCalledWith(
+            "Input required and not supplied: path"
+        );
+        expect(logWarningMock).toHaveBeenCalledTimes(1);
+        expect(failedMock).toHaveBeenCalledTimes(0);
+    }
+);
+
+test.each(refKeySet)(
+    "save with large cache outputs warning",
+    async (refKey, ref) => {
+        process.env[refKey] = ref;
+
+        const logWarningMock = jest.spyOn(actionUtils, "logWarning");
+        const failedMock = jest.spyOn(core, "setFailed");
+
+        const primaryKey =
+            "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
+        const cacheEntry: ArtifactCacheEntry = {
+            cacheKey: "Linux-node-",
+            scope: "refs/heads/master",
+            creationTime: "2019-11-13T19:18:02+00:00",
+            archiveLocation: "www.actionscache.test/download"
+        };
+
+        jest.spyOn(core, "getState")
+            // Cache Entry State
+            .mockImplementationOnce(() => {
+                return JSON.stringify(cacheEntry);
+            })
+            // Cache Key State
+            .mockImplementationOnce(() => {
+                return primaryKey;
+            });
+
+        const inputPath = "node_modules";
+        const cachePaths = [path.resolve(inputPath)];
+        testUtils.setInput(Inputs.Path, inputPath);
+
+        const createTarMock = jest.spyOn(tar, "createTar");
+
+        const cacheSize = 6 * 1024 * 1024 * 1024; //~6GB, over the 5GB limit
+        jest.spyOn(actionUtils, "getArchiveFileSize").mockImplementationOnce(
+            () => {
+                return cacheSize;
+            }
+        );
+        const compression = CompressionMethod.Gzip;
+        const getCompressionMock = jest
+            .spyOn(actionUtils, "getCompressionMethod")
+            .mockReturnValue(Promise.resolve(compression));
+
+        await run();
+
+        const archiveFolder = "/foo/bar";
+
+        expect(createTarMock).toHaveBeenCalledTimes(1);
+        expect(createTarMock).toHaveBeenCalledWith(
+            archiveFolder,
+            cachePaths,
+            compression
+        );
+        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);
+        expect(getCompressionMock).toHaveBeenCalledTimes(1);
+    }
+);
+
+test.each(refKeySet)(
+    "save with reserve cache failure outputs warning",
+    async (refKey, ref) => {
+        process.env[refKey] = ref;
+
+        const infoMock = jest.spyOn(core, "info");
+        const logWarningMock = jest.spyOn(actionUtils, "logWarning");
+        const failedMock = jest.spyOn(core, "setFailed");
+
+        const primaryKey =
+            "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
+        const cacheEntry: ArtifactCacheEntry = {
+            cacheKey: "Linux-node-",
+            scope: "refs/heads/master",
+            creationTime: "2019-11-13T19:18:02+00:00",
+            archiveLocation: "www.actionscache.test/download"
+        };
+
+        jest.spyOn(core, "getState")
+            // Cache Entry State
+            .mockImplementationOnce(() => {
+                return JSON.stringify(cacheEntry);
+            })
+            // Cache Key State
+            .mockImplementationOnce(() => {
+                return primaryKey;
+            });
+
+        const inputPath = "node_modules";
+        testUtils.setInput(Inputs.Path, inputPath);
+
+        const reserveCacheMock = jest
+            .spyOn(cacheHttpClient, "reserveCache")
+            .mockImplementationOnce(() => {
+                return Promise.resolve(-1);
+            });
+
+        const createTarMock = jest.spyOn(tar, "createTar");
+        const saveCacheMock = jest.spyOn(cacheHttpClient, "saveCache");
+        const compression = CompressionMethod.Zstd;
+        const getCompressionMock = jest
+            .spyOn(actionUtils, "getCompressionMethod")
+            .mockReturnValue(Promise.resolve(compression));
+
+        await run();
+
+        expect(reserveCacheMock).toHaveBeenCalledTimes(1);
+        expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, {
+            compressionMethod: compression
         });
 
-    await run();
+        expect(infoMock).toHaveBeenCalledWith(
+            `Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
+        );
 
-    expect(logWarningMock).toHaveBeenCalledWith(
-        `Error retrieving key from state.`
-    );
-    expect(logWarningMock).toHaveBeenCalledTimes(1);
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
+        expect(createTarMock).toHaveBeenCalledTimes(0);
+        expect(saveCacheMock).toHaveBeenCalledTimes(0);
+        expect(logWarningMock).toHaveBeenCalledTimes(0);
+        expect(failedMock).toHaveBeenCalledTimes(0);
+        expect(getCompressionMock).toHaveBeenCalledTimes(1);
+    }
+);
 
-test("save with exact match returns early", async () => {
-    const infoMock = jest.spyOn(core, "info");
-    const failedMock = jest.spyOn(core, "setFailed");
+test.each(refKeySet)(
+    "save with server error outputs warning",
+    async (refKey, ref) => {
+        process.env[refKey] = ref;
 
-    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    const cacheEntry: ArtifactCacheEntry = {
-        cacheKey: primaryKey,
-        scope: "refs/heads/master",
-        creationTime: "2019-11-13T19:18:02+00:00",
-        archiveLocation: "www.actionscache.test/download"
-    };
+        const logWarningMock = jest.spyOn(actionUtils, "logWarning");
+        const failedMock = jest.spyOn(core, "setFailed");
 
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return JSON.stringify(cacheEntry);
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
+        const primaryKey =
+            "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
+        const cacheEntry: ArtifactCacheEntry = {
+            cacheKey: "Linux-node-",
+            scope: "refs/heads/master",
+            creationTime: "2019-11-13T19:18:02+00:00",
+            archiveLocation: "www.actionscache.test/download"
+        };
+
+        jest.spyOn(core, "getState")
+            // Cache Entry State
+            .mockImplementationOnce(() => {
+                return JSON.stringify(cacheEntry);
+            })
+            // Cache Key State
+            .mockImplementationOnce(() => {
+                return primaryKey;
+            });
+
+        const inputPath = "node_modules";
+        const cachePaths = [path.resolve(inputPath)];
+        testUtils.setInput(Inputs.Path, inputPath);
+
+        const cacheId = 4;
+        const reserveCacheMock = jest
+            .spyOn(cacheHttpClient, "reserveCache")
+            .mockImplementationOnce(() => {
+                return Promise.resolve(cacheId);
+            });
+
+        const createTarMock = jest.spyOn(tar, "createTar");
+
+        const saveCacheMock = jest
+            .spyOn(cacheHttpClient, "saveCache")
+            .mockImplementationOnce(() => {
+                throw new Error("HTTP Error Occurred");
+            });
+        const compression = CompressionMethod.Zstd;
+        const getCompressionMock = jest
+            .spyOn(actionUtils, "getCompressionMethod")
+            .mockReturnValue(Promise.resolve(compression));
+
+        await run();
+
+        expect(reserveCacheMock).toHaveBeenCalledTimes(1);
+        expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, {
+            compressionMethod: compression
         });
 
-    const createTarMock = jest.spyOn(tar, "createTar");
+        const archiveFolder = "/foo/bar";
+        const archiveFile = path.join(archiveFolder, CacheFilename.Zstd);
 
-    await run();
+        expect(createTarMock).toHaveBeenCalledTimes(1);
+        expect(createTarMock).toHaveBeenCalledWith(
+            archiveFolder,
+            cachePaths,
+            compression
+        );
 
-    expect(infoMock).toHaveBeenCalledWith(
-        `Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
-    );
+        expect(saveCacheMock).toHaveBeenCalledTimes(1);
+        expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archiveFile);
 
-    expect(createTarMock).toHaveBeenCalledTimes(0);
+        expect(logWarningMock).toHaveBeenCalledTimes(1);
+        expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
 
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
+        expect(failedMock).toHaveBeenCalledTimes(0);
+        expect(getCompressionMock).toHaveBeenCalledTimes(1);
+    }
+);
 
-test("save with missing input outputs warning", async () => {
-    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
-    const failedMock = jest.spyOn(core, "setFailed");
+test.each(refKeySet)(
+    "save with valid inputs uploads a cache",
+    async (refKey, ref) => {
+        process.env[refKey] = ref;
 
-    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    const cacheEntry: ArtifactCacheEntry = {
-        cacheKey: "Linux-node-",
-        scope: "refs/heads/master",
-        creationTime: "2019-11-13T19:18:02+00:00",
-        archiveLocation: "www.actionscache.test/download"
-    };
+        const failedMock = jest.spyOn(core, "setFailed");
 
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return JSON.stringify(cacheEntry);
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
+        const primaryKey =
+            "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
+        const cacheEntry: ArtifactCacheEntry = {
+            cacheKey: "Linux-node-",
+            scope: "refs/heads/master",
+            creationTime: "2019-11-13T19:18:02+00:00",
+            archiveLocation: "www.actionscache.test/download"
+        };
+
+        jest.spyOn(core, "getState")
+            // Cache Entry State
+            .mockImplementationOnce(() => {
+                return JSON.stringify(cacheEntry);
+            })
+            // Cache Key State
+            .mockImplementationOnce(() => {
+                return primaryKey;
+            });
+
+        const inputPath = "node_modules";
+        const cachePaths = [path.resolve(inputPath)];
+        testUtils.setInput(Inputs.Path, inputPath);
+
+        const cacheId = 4;
+        const reserveCacheMock = jest
+            .spyOn(cacheHttpClient, "reserveCache")
+            .mockImplementationOnce(() => {
+                return Promise.resolve(cacheId);
+            });
+
+        const createTarMock = jest.spyOn(tar, "createTar");
+
+        const saveCacheMock = jest.spyOn(cacheHttpClient, "saveCache");
+        const compression = CompressionMethod.Zstd;
+        const getCompressionMock = jest
+            .spyOn(actionUtils, "getCompressionMethod")
+            .mockReturnValue(Promise.resolve(compression));
+
+        await run();
+
+        expect(reserveCacheMock).toHaveBeenCalledTimes(1);
+        expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, {
+            compressionMethod: compression
         });
 
-    await run();
+        const archiveFolder = "/foo/bar";
+        const archiveFile = path.join(archiveFolder, CacheFilename.Zstd);
 
-    expect(logWarningMock).toHaveBeenCalledWith(
-        "Input required and not supplied: path"
-    );
-    expect(logWarningMock).toHaveBeenCalledTimes(1);
-    expect(failedMock).toHaveBeenCalledTimes(0);
-});
+        expect(createTarMock).toHaveBeenCalledTimes(1);
+        expect(createTarMock).toHaveBeenCalledWith(
+            archiveFolder,
+            cachePaths,
+            compression
+        );
 
-test("save with large cache outputs warning", async () => {
-    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
-    const failedMock = jest.spyOn(core, "setFailed");
+        expect(saveCacheMock).toHaveBeenCalledTimes(1);
+        expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archiveFile);
 
-    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    const cacheEntry: ArtifactCacheEntry = {
-        cacheKey: "Linux-node-",
-        scope: "refs/heads/master",
-        creationTime: "2019-11-13T19:18:02+00:00",
-        archiveLocation: "www.actionscache.test/download"
-    };
-
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return JSON.stringify(cacheEntry);
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
-        });
-
-    const inputPath = "node_modules";
-    const cachePaths = [path.resolve(inputPath)];
-    testUtils.setInput(Inputs.Path, inputPath);
-
-    const createTarMock = jest.spyOn(tar, "createTar");
-
-    const cacheSize = 6 * 1024 * 1024 * 1024; //~6GB, over the 5GB limit
-    jest.spyOn(actionUtils, "getArchiveFileSize").mockImplementationOnce(() => {
-        return cacheSize;
-    });
-    const compression = CompressionMethod.Gzip;
-    const getCompressionMock = jest
-        .spyOn(actionUtils, "getCompressionMethod")
-        .mockReturnValue(Promise.resolve(compression));
-
-    await run();
-
-    const archiveFolder = "/foo/bar";
-
-    expect(createTarMock).toHaveBeenCalledTimes(1);
-    expect(createTarMock).toHaveBeenCalledWith(
-        archiveFolder,
-        cachePaths,
-        compression
-    );
-    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);
-    expect(getCompressionMock).toHaveBeenCalledTimes(1);
-});
-
-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 cacheEntry: ArtifactCacheEntry = {
-        cacheKey: "Linux-node-",
-        scope: "refs/heads/master",
-        creationTime: "2019-11-13T19:18:02+00:00",
-        archiveLocation: "www.actionscache.test/download"
-    };
-
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return JSON.stringify(cacheEntry);
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
-        });
-
-    const inputPath = "node_modules";
-    testUtils.setInput(Inputs.Path, inputPath);
-
-    const reserveCacheMock = jest
-        .spyOn(cacheHttpClient, "reserveCache")
-        .mockImplementationOnce(() => {
-            return Promise.resolve(-1);
-        });
-
-    const createTarMock = jest.spyOn(tar, "createTar");
-    const saveCacheMock = jest.spyOn(cacheHttpClient, "saveCache");
-    const compression = CompressionMethod.Zstd;
-    const getCompressionMock = jest
-        .spyOn(actionUtils, "getCompressionMethod")
-        .mockReturnValue(Promise.resolve(compression));
-
-    await run();
-
-    expect(reserveCacheMock).toHaveBeenCalledTimes(1);
-    expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, {
-        compressionMethod: compression
-    });
-
-    expect(infoMock).toHaveBeenCalledWith(
-        `Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
-    );
-
-    expect(createTarMock).toHaveBeenCalledTimes(0);
-    expect(saveCacheMock).toHaveBeenCalledTimes(0);
-    expect(logWarningMock).toHaveBeenCalledTimes(0);
-    expect(failedMock).toHaveBeenCalledTimes(0);
-    expect(getCompressionMock).toHaveBeenCalledTimes(1);
-});
-
-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 cacheEntry: ArtifactCacheEntry = {
-        cacheKey: "Linux-node-",
-        scope: "refs/heads/master",
-        creationTime: "2019-11-13T19:18:02+00:00",
-        archiveLocation: "www.actionscache.test/download"
-    };
-
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return JSON.stringify(cacheEntry);
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
-        });
-
-    const inputPath = "node_modules";
-    const cachePaths = [path.resolve(inputPath)];
-    testUtils.setInput(Inputs.Path, inputPath);
-
-    const cacheId = 4;
-    const reserveCacheMock = jest
-        .spyOn(cacheHttpClient, "reserveCache")
-        .mockImplementationOnce(() => {
-            return Promise.resolve(cacheId);
-        });
-
-    const createTarMock = jest.spyOn(tar, "createTar");
-
-    const saveCacheMock = jest
-        .spyOn(cacheHttpClient, "saveCache")
-        .mockImplementationOnce(() => {
-            throw new Error("HTTP Error Occurred");
-        });
-    const compression = CompressionMethod.Zstd;
-    const getCompressionMock = jest
-        .spyOn(actionUtils, "getCompressionMethod")
-        .mockReturnValue(Promise.resolve(compression));
-
-    await run();
-
-    expect(reserveCacheMock).toHaveBeenCalledTimes(1);
-    expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, {
-        compressionMethod: compression
-    });
-
-    const archiveFolder = "/foo/bar";
-    const archiveFile = path.join(archiveFolder, CacheFilename.Zstd);
-
-    expect(createTarMock).toHaveBeenCalledTimes(1);
-    expect(createTarMock).toHaveBeenCalledWith(
-        archiveFolder,
-        cachePaths,
-        compression
-    );
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(1);
-    expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archiveFile);
-
-    expect(logWarningMock).toHaveBeenCalledTimes(1);
-    expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
-
-    expect(failedMock).toHaveBeenCalledTimes(0);
-    expect(getCompressionMock).toHaveBeenCalledTimes(1);
-});
-
-test("save with valid inputs uploads a cache", async () => {
-    const failedMock = jest.spyOn(core, "setFailed");
-
-    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
-    const cacheEntry: ArtifactCacheEntry = {
-        cacheKey: "Linux-node-",
-        scope: "refs/heads/master",
-        creationTime: "2019-11-13T19:18:02+00:00",
-        archiveLocation: "www.actionscache.test/download"
-    };
-
-    jest.spyOn(core, "getState")
-        // Cache Entry State
-        .mockImplementationOnce(() => {
-            return JSON.stringify(cacheEntry);
-        })
-        // Cache Key State
-        .mockImplementationOnce(() => {
-            return primaryKey;
-        });
-
-    const inputPath = "node_modules";
-    const cachePaths = [path.resolve(inputPath)];
-    testUtils.setInput(Inputs.Path, inputPath);
-
-    const cacheId = 4;
-    const reserveCacheMock = jest
-        .spyOn(cacheHttpClient, "reserveCache")
-        .mockImplementationOnce(() => {
-            return Promise.resolve(cacheId);
-        });
-
-    const createTarMock = jest.spyOn(tar, "createTar");
-
-    const saveCacheMock = jest.spyOn(cacheHttpClient, "saveCache");
-    const compression = CompressionMethod.Zstd;
-    const getCompressionMock = jest
-        .spyOn(actionUtils, "getCompressionMethod")
-        .mockReturnValue(Promise.resolve(compression));
-
-    await run();
-
-    expect(reserveCacheMock).toHaveBeenCalledTimes(1);
-    expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey, {
-        compressionMethod: compression
-    });
-
-    const archiveFolder = "/foo/bar";
-    const archiveFile = path.join(archiveFolder, CacheFilename.Zstd);
-
-    expect(createTarMock).toHaveBeenCalledTimes(1);
-    expect(createTarMock).toHaveBeenCalledWith(
-        archiveFolder,
-        cachePaths,
-        compression
-    );
-
-    expect(saveCacheMock).toHaveBeenCalledTimes(1);
-    expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archiveFile);
-
-    expect(failedMock).toHaveBeenCalledTimes(0);
-    expect(getCompressionMock).toHaveBeenCalledTimes(1);
-});
+        expect(failedMock).toHaveBeenCalledTimes(0);
+        expect(getCompressionMock).toHaveBeenCalledTimes(1);
+    }
+);
diff --git a/dist/restore/index.js b/dist/restore/index.js
index 0ab3872..d1c1aec 100644
--- a/dist/restore/index.js
+++ b/dist/restore/index.js
@@ -3345,10 +3345,16 @@ function resolvePaths(patterns) {
     });
 }
 exports.resolvePaths = resolvePaths;
-// Cache token authorized for all events that are tied to a ref
+// Cache token authorized for events where a reference is defined
 // See GitHub Context https://help.github.com/actions/automating-your-workflow-with-github-actions/contexts-and-expression-syntax-for-github-actions#github-context
 function isValidEvent() {
-    return constants_1.RefKey in process.env && Boolean(process.env[constants_1.RefKey]);
+    for (let i = 0; i < constants_1.RefKeys.length; i++) {
+        let refKey = constants_1.RefKeys[i];
+        if (refKey in process.env) {
+            return Boolean(process.env[refKey]);
+        }
+    }
+    return false;
 }
 exports.isValidEvent = isValidEvent;
 function unlinkFile(path) {
@@ -4607,7 +4613,10 @@ var CompressionMethod;
 // over the socket during this period, the socket is destroyed and the download
 // is aborted.
 exports.SocketTimeout = 5000;
-exports.RefKey = "GITHUB_REF";
+exports.RefKeys = [
+    "ACTIONS_CACHE_REF",
+    "GITHUB_REF",
+];
 
 
 /***/ }),
diff --git a/dist/save/index.js b/dist/save/index.js
index ec2df96..0a2b38a 100644
--- a/dist/save/index.js
+++ b/dist/save/index.js
@@ -3345,10 +3345,16 @@ function resolvePaths(patterns) {
     });
 }
 exports.resolvePaths = resolvePaths;
-// Cache token authorized for all events that are tied to a ref
+// Cache token authorized for events where a reference is defined
 // See GitHub Context https://help.github.com/actions/automating-your-workflow-with-github-actions/contexts-and-expression-syntax-for-github-actions#github-context
 function isValidEvent() {
-    return constants_1.RefKey in process.env && Boolean(process.env[constants_1.RefKey]);
+    for (let i = 0; i < constants_1.RefKeys.length; i++) {
+        let refKey = constants_1.RefKeys[i];
+        if (refKey in process.env) {
+            return Boolean(process.env[refKey]);
+        }
+    }
+    return false;
 }
 exports.isValidEvent = isValidEvent;
 function unlinkFile(path) {
@@ -4694,7 +4700,10 @@ var CompressionMethod;
 // over the socket during this period, the socket is destroyed and the download
 // is aborted.
 exports.SocketTimeout = 5000;
-exports.RefKey = "GITHUB_REF";
+exports.RefKeys = [
+    "ACTIONS_CACHE_REF",
+    "GITHUB_REF",
+];
 
 
 /***/ }),
diff --git a/src/constants.ts b/src/constants.ts
index 4eb5ef5..08bf196 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -34,4 +34,4 @@ export enum CompressionMethod {
 // is aborted.
 export const SocketTimeout = 5000;
 
-export const RefKey = "GITHUB_REF";
+export const RefKeys = ["ACTIONS_CACHE_REF", "GITHUB_REF"];
diff --git a/src/utils/actionUtils.ts b/src/utils/actionUtils.ts
index 3b7a857..1c14c33 100644
--- a/src/utils/actionUtils.ts
+++ b/src/utils/actionUtils.ts
@@ -11,7 +11,7 @@ import {
     CacheFilename,
     CompressionMethod,
     Outputs,
-    RefKey,
+    RefKeys,
     State
 } from "../constants";
 import { ArtifactCacheEntry } from "../contracts";
@@ -108,10 +108,18 @@ export async function resolvePaths(patterns: string[]): Promise<string[]> {
     return paths;
 }
 
-// Cache token authorized for all events that are tied to a ref
+// Cache token authorized for events where a reference is defined
 // See GitHub Context https://help.github.com/actions/automating-your-workflow-with-github-actions/contexts-and-expression-syntax-for-github-actions#github-context
 export function isValidEvent(): boolean {
-    return RefKey in process.env && Boolean(process.env[RefKey]);
+    for (let i = 0; i < RefKeys.length; i++) {
+        let refKey = RefKeys[i];
+
+        if (refKey in process.env) {
+            return Boolean(process.env[refKey])
+        }
+    }
+
+    return false;
 }
 
 export function unlinkFile(path: fs.PathLike): Promise<void> {