From 480d8905164cd02fffff8d0759e8f8ddb1b592c5 Mon Sep 17 00:00:00 2001 From: Stuart Leeks Date: Thu, 8 May 2025 16:23:21 +0000 Subject: [PATCH 1/5] Add cache-primary-key, cache-matched-key to main action.yml --- action.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/action.yml b/action.yml index 7af7458..2ab0b31 100644 --- a/action.yml +++ b/action.yml @@ -37,6 +37,10 @@ inputs: outputs: cache-hit: description: 'A boolean value to indicate an exact match was found for the primary key' + cache-primary-key: + description: 'A resolved cache key for which cache match was attempted' + cache-matched-key: + description: 'Key of the cache that was restored, it could either be the primary key on cache-hit or a partial/complete match of one of the restore keys' runs: using: 'node20' main: 'dist/restore/index.js' From 91afe36e0a9a6d67f974b918ac750b3d8a146775 Mon Sep 17 00:00:00 2001 From: Stuart Leeks Date: Thu, 8 May 2025 16:51:55 +0000 Subject: [PATCH 2/5] Update non-null state provider to also output primary/matched keys --- __tests__/restore.test.ts | 29 ++++++++++++++++++----------- __tests__/restoreImpl.test.ts | 33 +++++++++++++++++++++------------ __tests__/stateProvider.test.ts | 2 +- src/stateProvider.ts | 20 +++++++++++--------- 4 files changed, 51 insertions(+), 33 deletions(-) diff --git a/__tests__/restore.test.ts b/__tests__/restore.test.ts index 250f7ef..dac797e 100644 --- a/__tests__/restore.test.ts +++ b/__tests__/restore.test.ts @@ -149,7 +149,7 @@ test("restore with cache found for key", async () => { const infoMock = jest.spyOn(core, "info"); const failedMock = jest.spyOn(core, "setFailed"); const stateMock = jest.spyOn(core, "saveState"); - const setCacheHitOutputMock = jest.spyOn(core, "setOutput"); + const setOutputMock = jest.spyOn(core, "setOutput"); const restoreCacheMock = jest .spyOn(cache, "restoreCache") .mockImplementationOnce(() => { @@ -173,8 +173,10 @@ test("restore with cache found for key", async () => { expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", key); expect(stateMock).toHaveBeenCalledTimes(2); - expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); - expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true"); + expect(setOutputMock).toHaveBeenCalledTimes(3); + expect(setOutputMock).toHaveBeenCalledWith("cache-hit", "true"); + expect(setOutputMock).toHaveBeenCalledWith("cache-primary-key", key); + expect(setOutputMock).toHaveBeenCalledWith("cache-matched-key", key); expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`); expect(failedMock).toHaveBeenCalledTimes(0); @@ -194,7 +196,7 @@ test("restore with cache found for restore key", async () => { const infoMock = jest.spyOn(core, "info"); const failedMock = jest.spyOn(core, "setFailed"); const stateMock = jest.spyOn(core, "saveState"); - const setCacheHitOutputMock = jest.spyOn(core, "setOutput"); + const setOutputMock = jest.spyOn(core, "setOutput"); const restoreCacheMock = jest .spyOn(cache, "restoreCache") .mockImplementationOnce(() => { @@ -218,8 +220,10 @@ test("restore with cache found for restore key", async () => { expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", restoreKey); expect(stateMock).toHaveBeenCalledTimes(2); - expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); - expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false"); + expect(setOutputMock).toHaveBeenCalledTimes(3); + expect(setOutputMock).toHaveBeenCalledWith("cache-hit", "false"); + expect(setOutputMock).toHaveBeenCalledWith("cache-primary-key", key); + expect(setOutputMock).toHaveBeenCalledWith("cache-matched-key", restoreKey); expect(infoMock).toHaveBeenCalledWith( `Cache restored from key: ${restoreKey}` ); @@ -239,7 +243,7 @@ test("Fail restore when fail on cache miss is enabled and primary + restore keys const failedMock = jest.spyOn(core, "setFailed"); const stateMock = jest.spyOn(core, "saveState"); - const setCacheHitOutputMock = jest.spyOn(core, "setOutput"); + const setOutputMock = jest.spyOn(core, "setOutput"); const restoreCacheMock = jest .spyOn(cache, "restoreCache") .mockImplementationOnce(() => { @@ -260,7 +264,8 @@ test("Fail restore when fail on cache miss is enabled and primary + restore keys ); expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); - expect(setCacheHitOutputMock).toHaveBeenCalledTimes(0); + expect(setOutputMock).toHaveBeenCalledTimes(1); + expect(setOutputMock).toHaveBeenCalledWith("cache-primary-key", key); expect(failedMock).toHaveBeenCalledWith( `Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${key}` @@ -282,7 +287,7 @@ test("restore when fail on cache miss is enabled and primary key doesn't match r const infoMock = jest.spyOn(core, "info"); const failedMock = jest.spyOn(core, "setFailed"); const stateMock = jest.spyOn(core, "saveState"); - const setCacheHitOutputMock = jest.spyOn(core, "setOutput"); + const setOutputMock = jest.spyOn(core, "setOutput"); const restoreCacheMock = jest .spyOn(cache, "restoreCache") .mockImplementationOnce(() => { @@ -306,8 +311,10 @@ test("restore when fail on cache miss is enabled and primary key doesn't match r expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", restoreKey); expect(stateMock).toHaveBeenCalledTimes(2); - expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); - expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false"); + expect(setOutputMock).toHaveBeenCalledTimes(3); + expect(setOutputMock).toHaveBeenCalledWith("cache-hit", "false"); + expect(setOutputMock).toHaveBeenCalledWith("cache-primary-key", key); + expect(setOutputMock).toHaveBeenCalledWith("cache-matched-key", restoreKey); expect(infoMock).toHaveBeenCalledWith( `Cache restored from key: ${restoreKey}` diff --git a/__tests__/restoreImpl.test.ts b/__tests__/restoreImpl.test.ts index 16f5f72..9318980 100644 --- a/__tests__/restoreImpl.test.ts +++ b/__tests__/restoreImpl.test.ts @@ -112,7 +112,7 @@ test("restore on GHES with AC available ", async () => { const infoMock = jest.spyOn(core, "info"); const failedMock = jest.spyOn(core, "setFailed"); const stateMock = jest.spyOn(core, "saveState"); - const setCacheHitOutputMock = jest.spyOn(core, "setOutput"); + const setOutputMock = jest.spyOn(core, "setOutput"); const restoreCacheMock = jest .spyOn(cache, "restoreCache") .mockImplementationOnce(() => { @@ -133,8 +133,10 @@ test("restore on GHES with AC available ", async () => { ); expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); - expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); - expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true"); + expect(setOutputMock).toHaveBeenCalledTimes(3); + expect(setOutputMock).toHaveBeenCalledWith("cache-hit", "true"); + expect(setOutputMock).toHaveBeenCalledWith("cache-primary-key", key); + expect(setOutputMock).toHaveBeenCalledWith("cache-matched-key", key); expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`); expect(failedMock).toHaveBeenCalledTimes(0); @@ -334,7 +336,7 @@ test("restore with cache found for key", async () => { const infoMock = jest.spyOn(core, "info"); const failedMock = jest.spyOn(core, "setFailed"); const stateMock = jest.spyOn(core, "saveState"); - const setCacheHitOutputMock = jest.spyOn(core, "setOutput"); + const setOutputMock = jest.spyOn(core, "setOutput"); const restoreCacheMock = jest .spyOn(cache, "restoreCache") .mockImplementationOnce(() => { @@ -355,8 +357,11 @@ test("restore with cache found for key", async () => { ); expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); - expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); - expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true"); + expect(setOutputMock).toHaveBeenCalledTimes(3); + expect(setOutputMock).toHaveBeenCalledWith("cache-hit", "true"); + expect(setOutputMock).toHaveBeenCalledWith("cache-primary-key", key); + expect(setOutputMock).toHaveBeenCalledWith("cache-matched-key", key); + expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`); expect(failedMock).toHaveBeenCalledTimes(0); @@ -376,7 +381,7 @@ test("restore with cache found for restore key", async () => { const infoMock = jest.spyOn(core, "info"); const failedMock = jest.spyOn(core, "setFailed"); const stateMock = jest.spyOn(core, "saveState"); - const setCacheHitOutputMock = jest.spyOn(core, "setOutput"); + const setOutputMock = jest.spyOn(core, "setOutput"); const restoreCacheMock = jest .spyOn(cache, "restoreCache") .mockImplementationOnce(() => { @@ -397,8 +402,10 @@ test("restore with cache found for restore key", async () => { ); expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); - expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); - expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false"); + expect(setOutputMock).toHaveBeenCalledTimes(3); + expect(setOutputMock).toHaveBeenCalledWith("cache-hit", "false"); + expect(setOutputMock).toHaveBeenCalledWith("cache-primary-key", key); + expect(setOutputMock).toHaveBeenCalledWith("cache-matched-key", restoreKey); expect(infoMock).toHaveBeenCalledWith( `Cache restored from key: ${restoreKey}` ); @@ -417,7 +424,7 @@ test("restore with lookup-only set", async () => { const infoMock = jest.spyOn(core, "info"); const failedMock = jest.spyOn(core, "setFailed"); const stateMock = jest.spyOn(core, "saveState"); - const setCacheHitOutputMock = jest.spyOn(core, "setOutput"); + const setOutputMock = jest.spyOn(core, "setOutput"); const restoreCacheMock = jest .spyOn(cache, "restoreCache") .mockImplementationOnce(() => { @@ -441,8 +448,10 @@ test("restore with lookup-only set", async () => { expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", key); expect(stateMock).toHaveBeenCalledTimes(2); - expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); - expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true"); + expect(setOutputMock).toHaveBeenCalledTimes(3); + expect(setOutputMock).toHaveBeenCalledWith("cache-hit", "true"); + expect(setOutputMock).toHaveBeenCalledWith("cache-primary-key", key); + expect(setOutputMock).toHaveBeenCalledWith("cache-matched-key", key); expect(infoMock).toHaveBeenCalledWith( `Cache found and can be restored from key: ${key}` diff --git a/__tests__/stateProvider.test.ts b/__tests__/stateProvider.test.ts index 255b5fb..d46ee8b 100644 --- a/__tests__/stateProvider.test.ts +++ b/__tests__/stateProvider.test.ts @@ -54,7 +54,7 @@ test("StateProvider saves states", async () => { expect(cacheStateValue).toBe(cacheMatchedKey); expect(getStateMock).toHaveBeenCalledTimes(2); expect(saveStateMock).toHaveBeenCalledTimes(2); - expect(setOutputMock).toHaveBeenCalledTimes(0); + expect(setOutputMock).toHaveBeenCalledTimes(2); }); test("NullStateProvider saves outputs", async () => { diff --git a/src/stateProvider.ts b/src/stateProvider.ts index beb41e5..d9a53aa 100644 --- a/src/stateProvider.ts +++ b/src/stateProvider.ts @@ -21,26 +21,28 @@ class StateProviderBase implements IStateProvider { } // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function - setState = (key: string, value: string) => {}; + setState = (key: string, value: string) => { }; // eslint-disable-next-line @typescript-eslint/no-unused-vars getState = (key: string) => ""; } export class StateProvider extends StateProviderBase { - setState = core.saveState; + setState = (key: string, value: string) => { core.saveState(key, value); stateToOutput(key, value); }; getState = core.getState; } +const stateToOutputMap = new Map([ + [State.CacheMatchedKey, Outputs.CacheMatchedKey], + [State.CachePrimaryKey, Outputs.CachePrimaryKey] +]); +function stateToOutput(key: string, value: string) { + core.setOutput(stateToOutputMap.get(key) as string, value); +} export class NullStateProvider extends StateProviderBase { - stateToOutputMap = new Map([ - [State.CacheMatchedKey, Outputs.CacheMatchedKey], - [State.CachePrimaryKey, Outputs.CachePrimaryKey] - ]); - setState = (key: string, value: string) => { - core.setOutput(this.stateToOutputMap.get(key) as string, value); - }; + setState = stateToOutput; // eslint-disable-next-line @typescript-eslint/no-unused-vars getState = (key: string) => ""; } + From c5c1c31345dd8a3b81f168d751e89847e658f4c7 Mon Sep 17 00:00:00 2001 From: Stuart Leeks Date: Thu, 8 May 2025 16:51:59 +0000 Subject: [PATCH 3/5] npm run build --- dist/restore-only/index.js | 17 +++++++++-------- dist/restore/index.js | 17 +++++++++-------- dist/save-only/index.js | 17 +++++++++-------- dist/save/index.js | 17 +++++++++-------- 4 files changed, 36 insertions(+), 32 deletions(-) diff --git a/dist/restore-only/index.js b/dist/restore-only/index.js index c42c938..da82f3f 100644 --- a/dist/restore-only/index.js +++ b/dist/restore-only/index.js @@ -65114,21 +65114,22 @@ class StateProviderBase { class StateProvider extends StateProviderBase { constructor() { super(...arguments); - this.setState = core.saveState; + this.setState = (key, value) => { core.saveState(key, value); stateToOutput(key, value); }; this.getState = core.getState; } } exports.StateProvider = StateProvider; +const stateToOutputMap = new Map([ + [constants_1.State.CacheMatchedKey, constants_1.Outputs.CacheMatchedKey], + [constants_1.State.CachePrimaryKey, constants_1.Outputs.CachePrimaryKey] +]); +function stateToOutput(key, value) { + core.setOutput(stateToOutputMap.get(key), value); +} class NullStateProvider extends StateProviderBase { constructor() { super(...arguments); - this.stateToOutputMap = new Map([ - [constants_1.State.CacheMatchedKey, constants_1.Outputs.CacheMatchedKey], - [constants_1.State.CachePrimaryKey, constants_1.Outputs.CachePrimaryKey] - ]); - this.setState = (key, value) => { - core.setOutput(this.stateToOutputMap.get(key), value); - }; + this.setState = stateToOutput; // eslint-disable-next-line @typescript-eslint/no-unused-vars this.getState = (key) => ""; } diff --git a/dist/restore/index.js b/dist/restore/index.js index 95f7849..9e2a513 100644 --- a/dist/restore/index.js +++ b/dist/restore/index.js @@ -65114,21 +65114,22 @@ class StateProviderBase { class StateProvider extends StateProviderBase { constructor() { super(...arguments); - this.setState = core.saveState; + this.setState = (key, value) => { core.saveState(key, value); stateToOutput(key, value); }; this.getState = core.getState; } } exports.StateProvider = StateProvider; +const stateToOutputMap = new Map([ + [constants_1.State.CacheMatchedKey, constants_1.Outputs.CacheMatchedKey], + [constants_1.State.CachePrimaryKey, constants_1.Outputs.CachePrimaryKey] +]); +function stateToOutput(key, value) { + core.setOutput(stateToOutputMap.get(key), value); +} class NullStateProvider extends StateProviderBase { constructor() { super(...arguments); - this.stateToOutputMap = new Map([ - [constants_1.State.CacheMatchedKey, constants_1.Outputs.CacheMatchedKey], - [constants_1.State.CachePrimaryKey, constants_1.Outputs.CachePrimaryKey] - ]); - this.setState = (key, value) => { - core.setOutput(this.stateToOutputMap.get(key), value); - }; + this.setState = stateToOutput; // eslint-disable-next-line @typescript-eslint/no-unused-vars this.getState = (key) => ""; } diff --git a/dist/save-only/index.js b/dist/save-only/index.js index 1e1b222..f191eb4 100644 --- a/dist/save-only/index.js +++ b/dist/save-only/index.js @@ -65127,21 +65127,22 @@ class StateProviderBase { class StateProvider extends StateProviderBase { constructor() { super(...arguments); - this.setState = core.saveState; + this.setState = (key, value) => { core.saveState(key, value); stateToOutput(key, value); }; this.getState = core.getState; } } exports.StateProvider = StateProvider; +const stateToOutputMap = new Map([ + [constants_1.State.CacheMatchedKey, constants_1.Outputs.CacheMatchedKey], + [constants_1.State.CachePrimaryKey, constants_1.Outputs.CachePrimaryKey] +]); +function stateToOutput(key, value) { + core.setOutput(stateToOutputMap.get(key), value); +} class NullStateProvider extends StateProviderBase { constructor() { super(...arguments); - this.stateToOutputMap = new Map([ - [constants_1.State.CacheMatchedKey, constants_1.Outputs.CacheMatchedKey], - [constants_1.State.CachePrimaryKey, constants_1.Outputs.CachePrimaryKey] - ]); - this.setState = (key, value) => { - core.setOutput(this.stateToOutputMap.get(key), value); - }; + this.setState = stateToOutput; // eslint-disable-next-line @typescript-eslint/no-unused-vars this.getState = (key) => ""; } diff --git a/dist/save/index.js b/dist/save/index.js index d288e0b..683caad 100644 --- a/dist/save/index.js +++ b/dist/save/index.js @@ -65127,21 +65127,22 @@ class StateProviderBase { class StateProvider extends StateProviderBase { constructor() { super(...arguments); - this.setState = core.saveState; + this.setState = (key, value) => { core.saveState(key, value); stateToOutput(key, value); }; this.getState = core.getState; } } exports.StateProvider = StateProvider; +const stateToOutputMap = new Map([ + [constants_1.State.CacheMatchedKey, constants_1.Outputs.CacheMatchedKey], + [constants_1.State.CachePrimaryKey, constants_1.Outputs.CachePrimaryKey] +]); +function stateToOutput(key, value) { + core.setOutput(stateToOutputMap.get(key), value); +} class NullStateProvider extends StateProviderBase { constructor() { super(...arguments); - this.stateToOutputMap = new Map([ - [constants_1.State.CacheMatchedKey, constants_1.Outputs.CacheMatchedKey], - [constants_1.State.CachePrimaryKey, constants_1.Outputs.CachePrimaryKey] - ]); - this.setState = (key, value) => { - core.setOutput(this.stateToOutputMap.get(key), value); - }; + this.setState = stateToOutput; // eslint-disable-next-line @typescript-eslint/no-unused-vars this.getState = (key) => ""; } From 6905c1168128c89664dec2d82d8ee222c47c9b58 Mon Sep 17 00:00:00 2001 From: Stuart Leeks Date: Thu, 8 May 2025 16:58:15 +0000 Subject: [PATCH 4/5] Update docs --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3f07f15..fe00f02 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,8 @@ If you are using a `self-hosted` Windows runner, `GNU tar` and `zstd` are requir * `cache-hit` - A string value to indicate an exact match was found for the key. * If there's a cache hit, this will be 'true' or 'false' to indicate if there's an exact match for `key`. * If there's a cache miss, this will be an empty string. +* `cache-primary-key` - Cache primary key passed in the input to use in subsequent steps of the workflow. +* `cache-matched-key` - Key of the cache that was restored, it could either be the primary key on cache-hit or a partial/complete match of one of the restore keys. See [Skipping steps based on cache-hit](#skipping-steps-based-on-cache-hit) for info on using this output From 783accdc1c503e1488bfdafbfc3a8740b5b9284f Mon Sep 17 00:00:00 2001 From: Stuart Leeks Date: Thu, 8 May 2025 17:01:34 +0000 Subject: [PATCH 5/5] lint/format --- __tests__/restoreImpl.test.ts | 1 - src/stateProvider.ts | 9 +++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/__tests__/restoreImpl.test.ts b/__tests__/restoreImpl.test.ts index 9318980..3ddc83c 100644 --- a/__tests__/restoreImpl.test.ts +++ b/__tests__/restoreImpl.test.ts @@ -362,7 +362,6 @@ test("restore with cache found for key", async () => { expect(setOutputMock).toHaveBeenCalledWith("cache-primary-key", key); expect(setOutputMock).toHaveBeenCalledWith("cache-matched-key", key); - expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`); expect(failedMock).toHaveBeenCalledTimes(0); }); diff --git a/src/stateProvider.ts b/src/stateProvider.ts index d9a53aa..f03f5ee 100644 --- a/src/stateProvider.ts +++ b/src/stateProvider.ts @@ -21,14 +21,17 @@ class StateProviderBase implements IStateProvider { } // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function - setState = (key: string, value: string) => { }; + setState = (key: string, value: string) => {}; // eslint-disable-next-line @typescript-eslint/no-unused-vars getState = (key: string) => ""; } export class StateProvider extends StateProviderBase { - setState = (key: string, value: string) => { core.saveState(key, value); stateToOutput(key, value); }; + setState = (key: string, value: string) => { + core.saveState(key, value); + stateToOutput(key, value); + }; getState = core.getState; } @@ -40,9 +43,7 @@ function stateToOutput(key: string, value: string) { core.setOutput(stateToOutputMap.get(key) as string, value); } export class NullStateProvider extends StateProviderBase { - setState = stateToOutput; // eslint-disable-next-line @typescript-eslint/no-unused-vars getState = (key: string) => ""; } -