From d35302b2257658b0d736bdc1a3326e4784b236d5 Mon Sep 17 00:00:00 2001
From: Gregorio Litenstein <g.litenstein@gmail.com>
Date: Fri, 7 Apr 2023 21:53:06 -0400
Subject: [PATCH] Allow refreshing cache also with granular save.

---
 __tests__/saveImpl.test.ts | 79 ++++++++++++++++++++++++++++++++++++--
 __tests__/saveOnly.test.ts | 39 +++++++++++++++++++
 dist/save-only/index.js    | 12 +++++-
 dist/save/index.js         | 12 +++++-
 src/restoreImpl.ts         |  1 -
 src/saveImpl.ts            | 20 +++++++++-
 6 files changed, 153 insertions(+), 10 deletions(-)

diff --git a/__tests__/saveImpl.test.ts b/__tests__/saveImpl.test.ts
index 42a4d4b..e3f0a7e 100644
--- a/__tests__/saveImpl.test.ts
+++ b/__tests__/saveImpl.test.ts
@@ -4,7 +4,7 @@ import nock from "nock";
 
 import { Events, Inputs, RefKey } from "../src/constants";
 import { saveImpl } from "../src/saveImpl";
-import { StateProvider } from "../src/stateProvider";
+import { NullStateProvider, StateProvider } from "../src/stateProvider";
 import * as actionUtils from "../src/utils/actionUtils";
 import * as testUtils from "../src/utils/testUtils";
 
@@ -471,7 +471,80 @@ test("save with cache hit and refresh-cache will try to delete and re-create ent
         .mockImplementationOnce(() => {
             return Promise.resolve(cacheId);
         });
-    await run(new StateProvider());
+    await saveImpl(new StateProvider());
+
+    expect(saveCacheMock).toHaveBeenCalledTimes(1);
+    expect(saveCacheMock).toHaveBeenCalledWith(
+        [inputPath],
+        primaryKey,
+        {
+            uploadChunkSize: 4000000
+        },
+        false
+    );
+
+    expect(logWarningMock).toHaveBeenCalledTimes(0);
+    expect(infoMock).toHaveBeenCalledTimes(3);
+
+    expect(infoMock).toHaveBeenNthCalledWith(
+        1,
+        `Cache hit occurred on the primary key ${primaryKey}, attempting to refresh the contents of the cache.`
+    );
+    expect(infoMock).toHaveBeenNthCalledWith(
+        2,
+        `Succesfully deleted cache with key: ${primaryKey}`
+    );
+    expect(infoMock).toHaveBeenNthCalledWith(
+        3,
+        `Cache saved with key: ${primaryKey}`
+    );
+
+    expect(failedMock).toHaveBeenCalledTimes(0);
+});
+
+test("Granular save will use lookup to determine if cache needs to be updated or (not) saved.", async () => {
+    process.env["GITHUB_REPOSITORY"] = "owner/repo";
+    process.env["GITHUB_TOKEN"] =
+        "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";
+    process.env["GITHUB_ACTION"] = "__owner___run-repo";
+
+    const infoMock = jest.spyOn(core, "info");
+    const logWarningMock = jest.spyOn(actionUtils, "logWarning");
+    const failedMock = jest.spyOn(core, "setFailed");
+
+    const primaryKey = testUtils.successCacheKey;
+
+    const inputPath = "node_modules";
+    testUtils.setInput(Inputs.Key, primaryKey);
+    testUtils.setInput(Inputs.RefreshCache, "true");
+    testUtils.setInput(Inputs.Path, inputPath);
+    testUtils.setInput(Inputs.UploadChunkSize, "4000000");
+
+    const restoreCacheMock = jest
+        .spyOn(cache, "restoreCache")
+        .mockImplementation(() => {
+            return Promise.resolve(primaryKey);
+        });
+
+    const cacheId = 4;
+    const saveCacheMock = jest
+        .spyOn(cache, "saveCache")
+        .mockImplementationOnce(() => {
+            return Promise.resolve(cacheId);
+        });
+
+    await saveImpl(new NullStateProvider());
+
+    expect(restoreCacheMock).toHaveBeenCalledTimes(1);
+    expect(restoreCacheMock).toHaveBeenCalledWith(
+        [inputPath],
+        primaryKey,
+        [],
+        {
+            lookupOnly: true
+        },
+        false
+    );
 
     expect(saveCacheMock).toHaveBeenCalledTimes(1);
     expect(saveCacheMock).toHaveBeenCalledWith(
@@ -524,7 +597,7 @@ test("save with cache hit and refresh-cache will throw a warning if there's no G
         });
 
     const saveCacheMock = jest.spyOn(cache, "saveCache");
-    await run(new StateProvider());
+    await saveImpl(new StateProvider());
 
     expect(saveCacheMock).toHaveBeenCalledTimes(0);
     expect(logWarningMock).toHaveBeenCalledWith(
diff --git a/__tests__/saveOnly.test.ts b/__tests__/saveOnly.test.ts
index 8589525..81e3a1f 100644
--- a/__tests__/saveOnly.test.ts
+++ b/__tests__/saveOnly.test.ts
@@ -111,6 +111,45 @@ test("save with valid inputs uploads a cache", async () => {
     expect(failedMock).toHaveBeenCalledTimes(0);
 });
 
+test("Granular save with refreshCache is able to save cache", async () => {
+    process.env["GITHUB_REPOSITORY"] = "owner/repo";
+    process.env["GITHUB_TOKEN"] =
+        "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";
+    process.env["GITHUB_ACTION"] = "__owner___run-repo";
+    const failedMock = jest.spyOn(core, "setFailed");
+
+    const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
+
+    const inputPath = "node_modules";
+    process.env.CACHE_RESTORE_ONLY_MATCHED_KEY = primaryKey;
+    testUtils.setInput(Inputs.Key, primaryKey);
+    testUtils.setInput(Inputs.RefreshCache, "true");
+    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 saveOnlyRun();
+
+    expect(saveCacheMock).toHaveBeenCalledTimes(1);
+    expect(saveCacheMock).toHaveBeenCalledWith(
+        [inputPath],
+        primaryKey,
+        {
+            uploadChunkSize: 4000000
+        },
+        false
+    );
+
+    expect(failedMock).toHaveBeenCalledTimes(0);
+});
+
 test("save failing logs the warning message", async () => {
     const warningMock = jest.spyOn(core, "warning");
 
diff --git a/dist/save-only/index.js b/dist/save-only/index.js
index b1f7567..7301e0c 100644
--- a/dist/save-only/index.js
+++ b/dist/save-only/index.js
@@ -77561,8 +77561,16 @@ function saveImpl(stateProvider) {
                 return;
             }
             const refreshCache = utils.getInputAsBool(constants_1.Inputs.RefreshCache, { required: false });
-            // If matched restore key is same as primary key, either try to refresh the cache, or just notify and do not save (NO-OP in case of SaveOnly action)
-            const restoredKey = stateProvider.getCacheState();
+            // If matched restore key is same as primary key, either try to refresh the cache, or just notify and do not save.
+            let restoredKey = stateProvider.getCacheState();
+            if (refreshCache && !restoredKey) {
+                // If getCacheState didn't give us a key, we're likely using granular actions. Do a lookup to see if we need to refresh or just do a regular save.
+                const cachePaths = utils.getInputAsArray(constants_1.Inputs.Path, {
+                    required: true
+                });
+                const enableCrossOsArchive = utils.getInputAsBool(constants_1.Inputs.EnableCrossOsArchive);
+                restoredKey = yield cache.restoreCache(cachePaths, primaryKey, [], { lookupOnly: true }, enableCrossOsArchive);
+            }
             if (utils.isExactKeyMatch(primaryKey, restoredKey)) {
                 const { GITHUB_TOKEN, GITHUB_REPOSITORY } = process.env || null;
                 if (GITHUB_TOKEN && GITHUB_REPOSITORY && refreshCache === true) {
diff --git a/dist/save/index.js b/dist/save/index.js
index 6dc7133..31a2278 100644
--- a/dist/save/index.js
+++ b/dist/save/index.js
@@ -77561,8 +77561,16 @@ function saveImpl(stateProvider) {
                 return;
             }
             const refreshCache = utils.getInputAsBool(constants_1.Inputs.RefreshCache, { required: false });
-            // If matched restore key is same as primary key, either try to refresh the cache, or just notify and do not save (NO-OP in case of SaveOnly action)
-            const restoredKey = stateProvider.getCacheState();
+            // If matched restore key is same as primary key, either try to refresh the cache, or just notify and do not save.
+            let restoredKey = stateProvider.getCacheState();
+            if (refreshCache && !restoredKey) {
+                // If getCacheState didn't give us a key, we're likely using granular actions. Do a lookup to see if we need to refresh or just do a regular save.
+                const cachePaths = utils.getInputAsArray(constants_1.Inputs.Path, {
+                    required: true
+                });
+                const enableCrossOsArchive = utils.getInputAsBool(constants_1.Inputs.EnableCrossOsArchive);
+                restoredKey = yield cache.restoreCache(cachePaths, primaryKey, [], { lookupOnly: true }, enableCrossOsArchive);
+            }
             if (utils.isExactKeyMatch(primaryKey, restoredKey)) {
                 const { GITHUB_TOKEN, GITHUB_REPOSITORY } = process.env || null;
                 if (GITHUB_TOKEN && GITHUB_REPOSITORY && refreshCache === true) {
diff --git a/src/restoreImpl.ts b/src/restoreImpl.ts
index 54bef49..dd3023a 100644
--- a/src/restoreImpl.ts
+++ b/src/restoreImpl.ts
@@ -82,7 +82,6 @@ export async function restoreImpl(
         } else {
             core.info(`Cache restored from key: ${cacheKey}`);
         }
-
         return cacheKey;
     } catch (error: unknown) {
         core.setFailed((error as Error).message);
diff --git a/src/saveImpl.ts b/src/saveImpl.ts
index eb92bf8..1ac1882 100644
--- a/src/saveImpl.ts
+++ b/src/saveImpl.ts
@@ -48,10 +48,26 @@ export async function saveImpl(
             { required: false }
         );
 
-        // If matched restore key is same as primary key, either try to refresh the cache, or just notify and do not save (NO-OP in case of SaveOnly action)
+        // If matched restore key is same as primary key, either try to refresh the cache, or just notify and do not save.
 
-        const restoredKey = stateProvider.getCacheState();
+        let restoredKey = stateProvider.getCacheState();
 
+        if (refreshCache && !restoredKey) {
+            // If getCacheState didn't give us a key, we're likely using granular actions. Do a lookup to see if we need to refresh or just do a regular save.
+            const cachePaths = utils.getInputAsArray(Inputs.Path, {
+                required: true
+            });
+            const enableCrossOsArchive = utils.getInputAsBool(
+                Inputs.EnableCrossOsArchive
+            );
+            restoredKey = await cache.restoreCache(
+                cachePaths,
+                primaryKey,
+                [],
+                { lookupOnly: true },
+                enableCrossOsArchive
+            );
+        }
         if (utils.isExactKeyMatch(primaryKey, restoredKey)) {
             const { GITHUB_TOKEN, GITHUB_REPOSITORY } = process.env || null;
             if (GITHUB_TOKEN && GITHUB_REPOSITORY && refreshCache === true) {