diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 2a53c19..3bc8d50 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -46,10 +46,10 @@ jobs: uses: actions/checkout@v3 - name: Generate files in working directory shell: bash - run: __tests__/create-cache-files.sh ${{ runner.os }} test-cache + run: __tests__/create-cache-files.sh ${{ runner.os }}-save test-cache - name: Generate files outside working directory shell: bash - run: __tests__/create-cache-files.sh ${{ runner.os }} ~/test-cache + run: __tests__/create-cache-files.sh ${{ runner.os }}-save ~/test-cache - name: Save cache uses: ./ with: @@ -76,10 +76,52 @@ jobs: ~/test-cache - name: Verify cache files in working directory shell: bash - run: __tests__/verify-cache-files.sh ${{ runner.os }} test-cache + run: __tests__/verify-cache-files.sh ${{ runner.os }}-save test-cache - name: Verify cache files outside working directory shell: bash - run: __tests__/verify-cache-files.sh ${{ runner.os }} ~/test-cache + run: __tests__/verify-cache-files.sh ${{ runner.os }}-save ~/test-cache + + # End to end with save-always + test-save-always-with-failure: + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + fail-fast: false + runs-on: ${{ matrix.os }} + continue-on-error: true + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Save cache + id: save-always + uses: ./ + with: + key: test-${{ runner.os }}-${{ github.run_id }}.${{ github.run_attempt }} + path: test-cache + save-always: true + - name: Generate files + shell: bash + run: | + __tests__/create-cache-files.sh ${{ runner.os }}-save-always test-cache + exit 1 + test-save-always-restore: + needs: test-save-always-with-failure + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macOS-latest] + fail-fast: false + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Restore cache + uses: ./ + with: + key: test-${{ runner.os }}-${{ github.run_id }}.${{ github.run_attempt }} + path: test-cache + - name: Verify cache + shell: bash + run: __tests__/verify-cache-files.sh ${{ runner.os }}-save-always test-cache # End to end with proxy test-proxy-save: diff --git a/README.md b/README.md index 3da1ae0..bc7027a 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ If you are using a `self-hosted` Windows runner, `GNU tar` and `zstd` are requir * `enableCrossOsArchive` - An optional boolean when enabled, allows Windows runners to save or restore caches that can be restored or saved respectively on other platforms. Default: `false` * `fail-on-cache-miss` - Fail the workflow if cache entry is not found. Default: `false` * `lookup-only` - If true, only checks if cache entry exists and skips download. Does not change save cache behavior. Default: `false` +* `save-always` - If true, always saves the cache, even if the job fails. Requires a [step `id`](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsid). Default: `false` #### Environment Variables diff --git a/__tests__/restore.test.ts b/__tests__/restore.test.ts index 250f7ef..1cfb246 100644 --- a/__tests__/restore.test.ts +++ b/__tests__/restore.test.ts @@ -173,8 +173,12 @@ test("restore with cache found for key", async () => { expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", key); expect(stateMock).toHaveBeenCalledTimes(2); - expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); + expect(setCacheHitOutputMock).toHaveBeenCalledTimes(2); expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true"); + expect(setCacheHitOutputMock).toHaveBeenCalledWith( + "save-always-d18d746b9", + "false" + ); expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`); expect(failedMock).toHaveBeenCalledTimes(0); @@ -218,8 +222,12 @@ 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).toHaveBeenCalledTimes(2); expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false"); + expect(setCacheHitOutputMock).toHaveBeenCalledWith( + "save-always-d18d746b9", + "false" + ); expect(infoMock).toHaveBeenCalledWith( `Cache restored from key: ${restoreKey}` ); @@ -260,7 +268,11 @@ 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(setCacheHitOutputMock).toHaveBeenCalledTimes(1); + expect(setCacheHitOutputMock).toHaveBeenCalledWith( + "save-always-d18d746b9", + "false" + ); expect(failedMock).toHaveBeenCalledWith( `Failed to restore cache entry. Exiting as fail-on-cache-miss is set. Input key: ${key}` @@ -306,8 +318,12 @@ 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).toHaveBeenCalledTimes(2); expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false"); + expect(setCacheHitOutputMock).toHaveBeenCalledWith( + "save-always-d18d746b9", + "false" + ); expect(infoMock).toHaveBeenCalledWith( `Cache restored from key: ${restoreKey}` diff --git a/__tests__/restoreImpl.test.ts b/__tests__/restoreImpl.test.ts index 16f5f72..75ed7e3 100644 --- a/__tests__/restoreImpl.test.ts +++ b/__tests__/restoreImpl.test.ts @@ -79,8 +79,12 @@ test("restore without AC available should no-op", async () => { await restoreImpl(new StateProvider()); expect(restoreCacheMock).toHaveBeenCalledTimes(0); - expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); + expect(setCacheHitOutputMock).toHaveBeenCalledTimes(2); expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false"); + expect(setCacheHitOutputMock).toHaveBeenCalledWith( + "save-always-d18d746b9", + "false" + ); }); test("restore on GHES without AC available should no-op", async () => { @@ -95,8 +99,12 @@ test("restore on GHES without AC available should no-op", async () => { await restoreImpl(new StateProvider()); expect(restoreCacheMock).toHaveBeenCalledTimes(0); - expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); + expect(setCacheHitOutputMock).toHaveBeenCalledTimes(2); expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false"); + expect(setCacheHitOutputMock).toHaveBeenCalledWith( + "save-always-d18d746b9", + "false" + ); }); test("restore on GHES with AC available ", async () => { @@ -133,8 +141,12 @@ test("restore on GHES with AC available ", async () => { ); expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); - expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); + expect(setCacheHitOutputMock).toHaveBeenCalledTimes(2); expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true"); + expect(setCacheHitOutputMock).toHaveBeenCalledWith( + "save-always-d18d746b9", + "false" + ); expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`); expect(failedMock).toHaveBeenCalledTimes(0); @@ -355,8 +367,12 @@ test("restore with cache found for key", async () => { ); expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); - expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); + expect(setCacheHitOutputMock).toHaveBeenCalledTimes(2); expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true"); + expect(setCacheHitOutputMock).toHaveBeenCalledWith( + "save-always-d18d746b9", + "false" + ); expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`); expect(failedMock).toHaveBeenCalledTimes(0); @@ -397,8 +413,12 @@ test("restore with cache found for restore key", async () => { ); expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); - expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); + expect(setCacheHitOutputMock).toHaveBeenCalledTimes(2); expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "false"); + expect(setCacheHitOutputMock).toHaveBeenCalledWith( + "save-always-d18d746b9", + "false" + ); expect(infoMock).toHaveBeenCalledWith( `Cache restored from key: ${restoreKey}` ); @@ -441,8 +461,12 @@ test("restore with lookup-only set", async () => { expect(stateMock).toHaveBeenCalledWith("CACHE_RESULT", key); expect(stateMock).toHaveBeenCalledTimes(2); - expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); + expect(setCacheHitOutputMock).toHaveBeenCalledTimes(2); expect(setCacheHitOutputMock).toHaveBeenCalledWith("cache-hit", "true"); + expect(setCacheHitOutputMock).toHaveBeenCalledWith( + "save-always-d18d746b9", + "false" + ); expect(infoMock).toHaveBeenCalledWith( `Cache found and can be restored from key: ${key}` @@ -465,3 +489,40 @@ test("restore failure with earlyExit should call process exit", async () => { ); expect(processExitMock).toHaveBeenCalledWith(1); }); + +test("restore with save-always set", async () => { + jest.spyOn(actionUtils, "isGhes").mockImplementation(() => true); + const path = "node_modules"; + const key = "node-test"; + testUtils.setInputs({ + path: path, + key, + saveAlways: true + }); + + const setSaveAlwaysOutputMock = jest.spyOn(core, "setOutput"); + const restoreCacheMock = jest + .spyOn(cache, "restoreCache") + .mockImplementationOnce(() => { + return Promise.resolve(undefined); + }); + + await restoreImpl(new StateProvider()); + + expect(restoreCacheMock).toHaveBeenCalledTimes(1); + expect(restoreCacheMock).toHaveBeenCalledWith( + [path], + key, + [], + { + lookupOnly: false + }, + false + ); + + expect(setSaveAlwaysOutputMock).toHaveBeenCalledTimes(1); + expect(setSaveAlwaysOutputMock).toHaveBeenCalledWith( + "save-always-d18d746b9", + "true" + ); +}); diff --git a/__tests__/restoreOnly.test.ts b/__tests__/restoreOnly.test.ts index 81e5bca..8a0eb41 100644 --- a/__tests__/restoreOnly.test.ts +++ b/__tests__/restoreOnly.test.ts @@ -86,7 +86,8 @@ test("restore with no cache found", async () => { ); expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key); - expect(outputMock).toHaveBeenCalledTimes(1); + expect(outputMock).toHaveBeenCalledWith("save-always-d18d746b9", "false"); + expect(outputMock).toHaveBeenCalledTimes(2); expect(failedMock).toHaveBeenCalledTimes(0); expect(infoMock).toHaveBeenCalledWith( @@ -169,8 +170,9 @@ test("restore with cache found for key", async () => { expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key); expect(outputMock).toHaveBeenCalledWith("cache-hit", "true"); expect(outputMock).toHaveBeenCalledWith("cache-matched-key", key); + expect(outputMock).toHaveBeenCalledWith("save-always-d18d746b9", "false"); - expect(outputMock).toHaveBeenCalledTimes(3); + expect(outputMock).toHaveBeenCalledTimes(4); expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`); expect(failedMock).toHaveBeenCalledTimes(0); @@ -212,8 +214,9 @@ test("restore with cache found for restore key", async () => { expect(outputMock).toHaveBeenCalledWith("cache-primary-key", key); expect(outputMock).toHaveBeenCalledWith("cache-hit", "false"); expect(outputMock).toHaveBeenCalledWith("cache-matched-key", restoreKey); + expect(outputMock).toHaveBeenCalledWith("save-always-d18d746b9", "false"); - expect(outputMock).toHaveBeenCalledTimes(3); + expect(outputMock).toHaveBeenCalledTimes(4); expect(infoMock).toHaveBeenCalledWith( `Cache restored from key: ${restoreKey}` diff --git a/action.yml b/action.yml index 0125281..2b51484 100644 --- a/action.yml +++ b/action.yml @@ -33,11 +33,13 @@ inputs: outputs: cache-hit: description: 'A boolean value to indicate an exact match was found for the primary key' + save-always-d18d746b9: + description: "Run the post step to save the cache even if another step before fails" runs: using: 'node20' main: 'dist/restore/index.js' post: 'dist/save/index.js' - post-if: "success() || github.event.inputs.save-always" + post-if: "success() || (contains(steps.*.outputs.save-always-d18d746b9, 'true') && !contains(steps.*.outputs.save-always-d18d746b9, 'false'))" branding: icon: 'archive' color: 'gray-dark' diff --git a/dist/restore-only/index.js b/dist/restore-only/index.js index 9a59ac0..d16d713 100644 --- a/dist/restore-only/index.js +++ b/dist/restore-only/index.js @@ -59324,13 +59324,15 @@ var Inputs; Inputs["UploadChunkSize"] = "upload-chunk-size"; Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; Inputs["FailOnCacheMiss"] = "fail-on-cache-miss"; - Inputs["LookupOnly"] = "lookup-only"; // Input for cache, restore action + Inputs["LookupOnly"] = "lookup-only"; + Inputs["SaveAlways"] = "save-always"; // Input for cache action })(Inputs = exports.Inputs || (exports.Inputs = {})); var Outputs; (function (Outputs) { Outputs["CacheHit"] = "cache-hit"; Outputs["CachePrimaryKey"] = "cache-primary-key"; - Outputs["CacheMatchedKey"] = "cache-matched-key"; // Output from restore action + Outputs["CacheMatchedKey"] = "cache-matched-key"; + Outputs["SaveAlways"] = "save-always-d18d746b9"; // Output from cache action, with unique suffix for detection in post-if })(Outputs = exports.Outputs || (exports.Outputs = {})); var State; (function (State) { @@ -59394,6 +59396,7 @@ const stateProvider_1 = __nccwpck_require__(1527); const utils = __importStar(__nccwpck_require__(6850)); function restoreImpl(stateProvider, earlyExit) { return __awaiter(this, void 0, void 0, function* () { + core.setOutput(constants_1.Outputs.SaveAlways, core.getInput(constants_1.Inputs.SaveAlways) || "false"); try { if (!utils.isCacheFeatureAvailable()) { core.setOutput(constants_1.Outputs.CacheHit, "false"); diff --git a/dist/restore/index.js b/dist/restore/index.js index 03a12b5..e593b03 100644 --- a/dist/restore/index.js +++ b/dist/restore/index.js @@ -59324,13 +59324,15 @@ var Inputs; Inputs["UploadChunkSize"] = "upload-chunk-size"; Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; Inputs["FailOnCacheMiss"] = "fail-on-cache-miss"; - Inputs["LookupOnly"] = "lookup-only"; // Input for cache, restore action + Inputs["LookupOnly"] = "lookup-only"; + Inputs["SaveAlways"] = "save-always"; // Input for cache action })(Inputs = exports.Inputs || (exports.Inputs = {})); var Outputs; (function (Outputs) { Outputs["CacheHit"] = "cache-hit"; Outputs["CachePrimaryKey"] = "cache-primary-key"; - Outputs["CacheMatchedKey"] = "cache-matched-key"; // Output from restore action + Outputs["CacheMatchedKey"] = "cache-matched-key"; + Outputs["SaveAlways"] = "save-always-d18d746b9"; // Output from cache action, with unique suffix for detection in post-if })(Outputs = exports.Outputs || (exports.Outputs = {})); var State; (function (State) { @@ -59394,6 +59396,7 @@ const stateProvider_1 = __nccwpck_require__(1527); const utils = __importStar(__nccwpck_require__(6850)); function restoreImpl(stateProvider, earlyExit) { return __awaiter(this, void 0, void 0, function* () { + core.setOutput(constants_1.Outputs.SaveAlways, core.getInput(constants_1.Inputs.SaveAlways) || "false"); try { if (!utils.isCacheFeatureAvailable()) { core.setOutput(constants_1.Outputs.CacheHit, "false"); diff --git a/dist/save-only/index.js b/dist/save-only/index.js index f542b2c..ab1a399 100644 --- a/dist/save-only/index.js +++ b/dist/save-only/index.js @@ -59324,13 +59324,15 @@ var Inputs; Inputs["UploadChunkSize"] = "upload-chunk-size"; Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; Inputs["FailOnCacheMiss"] = "fail-on-cache-miss"; - Inputs["LookupOnly"] = "lookup-only"; // Input for cache, restore action + Inputs["LookupOnly"] = "lookup-only"; + Inputs["SaveAlways"] = "save-always"; // Input for cache action })(Inputs = exports.Inputs || (exports.Inputs = {})); var Outputs; (function (Outputs) { Outputs["CacheHit"] = "cache-hit"; Outputs["CachePrimaryKey"] = "cache-primary-key"; - Outputs["CacheMatchedKey"] = "cache-matched-key"; // Output from restore action + Outputs["CacheMatchedKey"] = "cache-matched-key"; + Outputs["SaveAlways"] = "save-always-d18d746b9"; // Output from cache action, with unique suffix for detection in post-if })(Outputs = exports.Outputs || (exports.Outputs = {})); var State; (function (State) { diff --git a/dist/save/index.js b/dist/save/index.js index f137655..4906ce9 100644 --- a/dist/save/index.js +++ b/dist/save/index.js @@ -59324,13 +59324,15 @@ var Inputs; Inputs["UploadChunkSize"] = "upload-chunk-size"; Inputs["EnableCrossOsArchive"] = "enableCrossOsArchive"; Inputs["FailOnCacheMiss"] = "fail-on-cache-miss"; - Inputs["LookupOnly"] = "lookup-only"; // Input for cache, restore action + Inputs["LookupOnly"] = "lookup-only"; + Inputs["SaveAlways"] = "save-always"; // Input for cache action })(Inputs = exports.Inputs || (exports.Inputs = {})); var Outputs; (function (Outputs) { Outputs["CacheHit"] = "cache-hit"; Outputs["CachePrimaryKey"] = "cache-primary-key"; - Outputs["CacheMatchedKey"] = "cache-matched-key"; // Output from restore action + Outputs["CacheMatchedKey"] = "cache-matched-key"; + Outputs["SaveAlways"] = "save-always-d18d746b9"; // Output from cache action, with unique suffix for detection in post-if })(Outputs = exports.Outputs || (exports.Outputs = {})); var State; (function (State) { diff --git a/src/constants.ts b/src/constants.ts index 0158ae0..d61d0cd 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -5,13 +5,15 @@ export enum Inputs { UploadChunkSize = "upload-chunk-size", // Input for cache, save action EnableCrossOsArchive = "enableCrossOsArchive", // Input for cache, restore, save action FailOnCacheMiss = "fail-on-cache-miss", // Input for cache, restore action - LookupOnly = "lookup-only" // Input for cache, restore action + LookupOnly = "lookup-only", // Input for cache, restore action + SaveAlways = "save-always" // Input for cache action } export enum Outputs { CacheHit = "cache-hit", // Output from cache, restore action CachePrimaryKey = "cache-primary-key", // Output from restore action - CacheMatchedKey = "cache-matched-key" // Output from restore action + CacheMatchedKey = "cache-matched-key", // Output from restore action + SaveAlways = "save-always-d18d746b9" // Output from cache action, with unique suffix for detection in post-if } export enum State { diff --git a/src/restoreImpl.ts b/src/restoreImpl.ts index 74a366d..b71a0bd 100644 --- a/src/restoreImpl.ts +++ b/src/restoreImpl.ts @@ -13,6 +13,11 @@ export async function restoreImpl( stateProvider: IStateProvider, earlyExit?: boolean | undefined ): Promise { + core.setOutput( + Outputs.SaveAlways, + core.getInput(Inputs.SaveAlways) || "false" + ); + try { if (!utils.isCacheFeatureAvailable()) { core.setOutput(Outputs.CacheHit, "false"); diff --git a/src/utils/testUtils.ts b/src/utils/testUtils.ts index ba0670b..84aeae9 100644 --- a/src/utils/testUtils.ts +++ b/src/utils/testUtils.ts @@ -16,6 +16,7 @@ interface CacheInput { enableCrossOsArchive?: boolean; failOnCacheMiss?: boolean; lookupOnly?: boolean; + saveAlways?: boolean; } export function setInputs(input: CacheInput): void { @@ -32,6 +33,8 @@ export function setInputs(input: CacheInput): void { setInput(Inputs.FailOnCacheMiss, input.failOnCacheMiss.toString()); input.lookupOnly !== undefined && setInput(Inputs.LookupOnly, input.lookupOnly.toString()); + input.saveAlways !== undefined && + setInput(Inputs.SaveAlways, input.saveAlways.toString()); } export function clearInputs(): void { @@ -42,4 +45,5 @@ export function clearInputs(): void { delete process.env[getInputName(Inputs.EnableCrossOsArchive)]; delete process.env[getInputName(Inputs.FailOnCacheMiss)]; delete process.env[getInputName(Inputs.LookupOnly)]; + delete process.env[getInputName(Inputs.SaveAlways)]; }