mirror of
				https://github.com/actions/cache.git
				synced 2025-10-31 18:34:19 +08:00 
			
		
		
		
	Merge master into releases/v1
This commit is contained in:
		
							parent
							
								
									86dff562ab
								
							
						
					
					
						commit
						6491e51b66
					
				
							
								
								
									
										16
									
								
								.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								.eslintrc.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,16 @@ | ||||
| { | ||||
|   "env": { "node": true, "jest": true }, | ||||
|   "parser": "@typescript-eslint/parser", | ||||
|   "parserOptions": { "ecmaVersion": 2020, "sourceType": "module" }, | ||||
|   "extends": [ | ||||
|     "eslint:recommended", | ||||
|     "plugin:@typescript-eslint/eslint-recommended", | ||||
|     "plugin:@typescript-eslint/recommended", | ||||
|     "plugin:import/errors", | ||||
|     "plugin:import/warnings", | ||||
|     "plugin:import/typescript", | ||||
|     "plugin:prettier/recommended", | ||||
|     "prettier/@typescript-eslint" | ||||
|   ], | ||||
|   "plugins": ["@typescript-eslint", "jest"] | ||||
| } | ||||
							
								
								
									
										22
									
								
								.github/workflows/workflow.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.github/workflows/workflow.yml
									
									
									
									
										vendored
									
									
								
							| @ -1,6 +1,11 @@ | ||||
| name: Tests | ||||
| 
 | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - master | ||||
|     paths-ignore: | ||||
|       - '**.md' | ||||
|   push: | ||||
|     branches: | ||||
|       - master | ||||
| @ -10,9 +15,11 @@ on: | ||||
| jobs: | ||||
|   test: | ||||
|     name: Test on ${{ matrix.os }} | ||||
| 
 | ||||
|     strategy: | ||||
|       matrix: | ||||
|         os: [ubuntu-latest, windows-latest, macOS-latest] | ||||
| 
 | ||||
|     runs-on: ${{ matrix.os }} | ||||
| 
 | ||||
|     steps: | ||||
| @ -22,10 +29,25 @@ jobs: | ||||
|       with: | ||||
|         node-version: '12.x' | ||||
| 
 | ||||
|     - name: Get npm cache directory | ||||
|       id: npm-cache | ||||
|       run: | | ||||
|         echo "::set-output name=dir::$(npm config get cache)" | ||||
| 
 | ||||
|     - uses: actions/cache@v1 | ||||
|       with: | ||||
|         path: ${{ steps.npm-cache.outputs.dir }} | ||||
|         key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} | ||||
|         restore-keys: | | ||||
|           ${{ runner.os }}-node- | ||||
| 
 | ||||
|     - run: npm ci | ||||
| 
 | ||||
|     - name: Prettier Format Check | ||||
|       run: npm run format-check | ||||
| 
 | ||||
|     - name: ESLint Check | ||||
|       run: npm run lint | ||||
| 
 | ||||
|     - name: Build & Test | ||||
|       run: npm run test | ||||
|  | ||||
							
								
								
									
										23
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								README.md
									
									
									
									
									
								
							| @ -28,7 +28,7 @@ Create a workflow `.yml` file in your repositories `.github/workflows` directory | ||||
| ### Example workflow | ||||
| 
 | ||||
| ```yaml | ||||
| name: Example Caching with npm | ||||
| name: Caching Primes | ||||
| 
 | ||||
| on: push | ||||
| 
 | ||||
| @ -39,22 +39,19 @@ jobs: | ||||
|     steps: | ||||
|     - uses: actions/checkout@v1 | ||||
| 
 | ||||
|     - name: Cache node modules | ||||
|     - name: Cache Primes | ||||
|       id: cache-primes | ||||
|       uses: actions/cache@v1 | ||||
|       with: | ||||
|         path: node_modules | ||||
|         key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} | ||||
|         restore-keys: | | ||||
|           ${{ runner.os }}-node- | ||||
|         path: prime-numbers | ||||
|         key: ${{ runner.os }}-primes | ||||
| 
 | ||||
|     - name: Install Dependencies | ||||
|       run: npm install | ||||
|     - name: Generate Prime Numbers | ||||
|       if: steps.cache-primes.outputs.cache-hit != 'true' | ||||
|       run: /generate-primes.sh -d prime-numbers | ||||
|      | ||||
|     - name: Build | ||||
|       run: npm run build | ||||
| 
 | ||||
|     - name: Test | ||||
|       run: npm run test | ||||
|     - name: Use Prime Numbers | ||||
|       run: /primes.sh -d prime-numbers | ||||
| ``` | ||||
| 
 | ||||
| ## Ecosystem Examples | ||||
|  | ||||
							
								
								
									
										1
									
								
								__tests__/__fixtures__/helloWorld.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								__tests__/__fixtures__/helloWorld.txt
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| hello world | ||||
							
								
								
									
										226
									
								
								__tests__/actionUtils.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										226
									
								
								__tests__/actionUtils.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,226 @@ | ||||
| import * as core from "@actions/core"; | ||||
| import * as os from "os"; | ||||
| import * as path from "path"; | ||||
| 
 | ||||
| import { Events, Outputs, State } from "../src/constants"; | ||||
| import { ArtifactCacheEntry } from "../src/contracts"; | ||||
| import * as actionUtils from "../src/utils/actionUtils"; | ||||
| 
 | ||||
| jest.mock("@actions/core"); | ||||
| jest.mock("os"); | ||||
| 
 | ||||
| afterEach(() => { | ||||
|     delete process.env[Events.Key]; | ||||
| }); | ||||
| 
 | ||||
| test("getArchiveFileSize returns file size", () => { | ||||
|     const filePath = path.join(__dirname, "__fixtures__", "helloWorld.txt"); | ||||
| 
 | ||||
|     const size = actionUtils.getArchiveFileSize(filePath); | ||||
| 
 | ||||
|     expect(size).toBe(11); | ||||
| }); | ||||
| 
 | ||||
| test("isExactKeyMatch with undefined cache entry returns false", () => { | ||||
|     const key = "linux-rust"; | ||||
|     const cacheEntry = undefined; | ||||
| 
 | ||||
|     expect(actionUtils.isExactKeyMatch(key, cacheEntry)).toBe(false); | ||||
| }); | ||||
| 
 | ||||
| test("isExactKeyMatch with empty cache entry returns false", () => { | ||||
|     const key = "linux-rust"; | ||||
|     const cacheEntry: ArtifactCacheEntry = {}; | ||||
| 
 | ||||
|     expect(actionUtils.isExactKeyMatch(key, cacheEntry)).toBe(false); | ||||
| }); | ||||
| 
 | ||||
| test("isExactKeyMatch with different keys returns false", () => { | ||||
|     const key = "linux-rust"; | ||||
|     const cacheEntry: ArtifactCacheEntry = { | ||||
|         cacheKey: "linux-" | ||||
|     }; | ||||
| 
 | ||||
|     expect(actionUtils.isExactKeyMatch(key, cacheEntry)).toBe(false); | ||||
| }); | ||||
| 
 | ||||
| test("isExactKeyMatch with different key accents returns false", () => { | ||||
|     const key = "linux-áccent"; | ||||
|     const cacheEntry: ArtifactCacheEntry = { | ||||
|         cacheKey: "linux-accent" | ||||
|     }; | ||||
| 
 | ||||
|     expect(actionUtils.isExactKeyMatch(key, cacheEntry)).toBe(false); | ||||
| }); | ||||
| 
 | ||||
| test("isExactKeyMatch with same key returns true", () => { | ||||
|     const key = "linux-rust"; | ||||
|     const cacheEntry: ArtifactCacheEntry = { | ||||
|         cacheKey: "linux-rust" | ||||
|     }; | ||||
| 
 | ||||
|     expect(actionUtils.isExactKeyMatch(key, cacheEntry)).toBe(true); | ||||
| }); | ||||
| 
 | ||||
| test("isExactKeyMatch with same key and different casing returns true", () => { | ||||
|     const key = "linux-rust"; | ||||
|     const cacheEntry: ArtifactCacheEntry = { | ||||
|         cacheKey: "LINUX-RUST" | ||||
|     }; | ||||
| 
 | ||||
|     expect(actionUtils.isExactKeyMatch(key, cacheEntry)).toBe(true); | ||||
| }); | ||||
| 
 | ||||
| test("setOutputAndState with undefined entry to set cache-hit output", () => { | ||||
|     const key = "linux-rust"; | ||||
|     const cacheEntry = undefined; | ||||
| 
 | ||||
|     const setOutputMock = jest.spyOn(core, "setOutput"); | ||||
|     const saveStateMock = jest.spyOn(core, "saveState"); | ||||
| 
 | ||||
|     actionUtils.setOutputAndState(key, cacheEntry); | ||||
| 
 | ||||
|     expect(setOutputMock).toHaveBeenCalledWith(Outputs.CacheHit, "false"); | ||||
|     expect(setOutputMock).toHaveBeenCalledTimes(1); | ||||
| 
 | ||||
|     expect(saveStateMock).toHaveBeenCalledTimes(0); | ||||
| }); | ||||
| 
 | ||||
| test("setOutputAndState with exact match to set cache-hit output and state", () => { | ||||
|     const key = "linux-rust"; | ||||
|     const cacheEntry: ArtifactCacheEntry = { | ||||
|         cacheKey: "linux-rust" | ||||
|     }; | ||||
| 
 | ||||
|     const setOutputMock = jest.spyOn(core, "setOutput"); | ||||
|     const saveStateMock = jest.spyOn(core, "saveState"); | ||||
| 
 | ||||
|     actionUtils.setOutputAndState(key, cacheEntry); | ||||
| 
 | ||||
|     expect(setOutputMock).toHaveBeenCalledWith(Outputs.CacheHit, "true"); | ||||
|     expect(setOutputMock).toHaveBeenCalledTimes(1); | ||||
| 
 | ||||
|     expect(saveStateMock).toHaveBeenCalledWith( | ||||
|         State.CacheResult, | ||||
|         JSON.stringify(cacheEntry) | ||||
|     ); | ||||
|     expect(saveStateMock).toHaveBeenCalledTimes(1); | ||||
| }); | ||||
| 
 | ||||
| test("setOutputAndState with no exact match to set cache-hit output and state", () => { | ||||
|     const key = "linux-rust"; | ||||
|     const cacheEntry: ArtifactCacheEntry = { | ||||
|         cacheKey: "linux-rust-bb828da54c148048dd17899ba9fda624811cfb43" | ||||
|     }; | ||||
| 
 | ||||
|     const setOutputMock = jest.spyOn(core, "setOutput"); | ||||
|     const saveStateMock = jest.spyOn(core, "saveState"); | ||||
| 
 | ||||
|     actionUtils.setOutputAndState(key, cacheEntry); | ||||
| 
 | ||||
|     expect(setOutputMock).toHaveBeenCalledWith(Outputs.CacheHit, "false"); | ||||
|     expect(setOutputMock).toHaveBeenCalledTimes(1); | ||||
| 
 | ||||
|     expect(saveStateMock).toHaveBeenCalledWith( | ||||
|         State.CacheResult, | ||||
|         JSON.stringify(cacheEntry) | ||||
|     ); | ||||
|     expect(saveStateMock).toHaveBeenCalledTimes(1); | ||||
| }); | ||||
| 
 | ||||
| test("getCacheState with no state returns undefined", () => { | ||||
|     const getStateMock = jest.spyOn(core, "getState"); | ||||
|     getStateMock.mockImplementation(() => { | ||||
|         return ""; | ||||
|     }); | ||||
| 
 | ||||
|     const state = actionUtils.getCacheState(); | ||||
| 
 | ||||
|     expect(state).toBe(undefined); | ||||
| 
 | ||||
|     expect(getStateMock).toHaveBeenCalledWith(State.CacheResult); | ||||
|     expect(getStateMock).toHaveBeenCalledTimes(1); | ||||
| }); | ||||
| 
 | ||||
| test("getCacheState with valid state", () => { | ||||
|     const cacheEntry: ArtifactCacheEntry = { | ||||
|         cacheKey: "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43", | ||||
|         scope: "refs/heads/master", | ||||
|         creationTime: "2019-11-13T19:18:02+00:00", | ||||
|         archiveLocation: "www.actionscache.test/download" | ||||
|     }; | ||||
|     const getStateMock = jest.spyOn(core, "getState"); | ||||
|     getStateMock.mockImplementation(() => { | ||||
|         return JSON.stringify(cacheEntry); | ||||
|     }); | ||||
| 
 | ||||
|     const state = actionUtils.getCacheState(); | ||||
| 
 | ||||
|     expect(state).toEqual(cacheEntry); | ||||
| 
 | ||||
|     expect(getStateMock).toHaveBeenCalledWith(State.CacheResult); | ||||
|     expect(getStateMock).toHaveBeenCalledTimes(1); | ||||
| }); | ||||
| 
 | ||||
| test("isValidEvent returns false for unknown event", () => { | ||||
|     const event = "foo"; | ||||
|     process.env[Events.Key] = event; | ||||
| 
 | ||||
|     const isValidEvent = actionUtils.isValidEvent(); | ||||
| 
 | ||||
|     expect(isValidEvent).toBe(false); | ||||
| }); | ||||
| 
 | ||||
| test("resolvePath with no ~ in path", () => { | ||||
|     const filePath = ".cache/yarn"; | ||||
| 
 | ||||
|     const resolvedPath = actionUtils.resolvePath(filePath); | ||||
| 
 | ||||
|     const expectedPath = path.resolve(filePath); | ||||
|     expect(resolvedPath).toBe(expectedPath); | ||||
| }); | ||||
| 
 | ||||
| test("resolvePath with ~ in path", () => { | ||||
|     const filePath = "~/.cache/yarn"; | ||||
| 
 | ||||
|     const homedir = jest.requireActual("os").homedir(); | ||||
|     const homedirMock = jest.spyOn(os, "homedir"); | ||||
|     homedirMock.mockImplementation(() => { | ||||
|         return homedir; | ||||
|     }); | ||||
| 
 | ||||
|     const resolvedPath = actionUtils.resolvePath(filePath); | ||||
| 
 | ||||
|     const expectedPath = path.join(homedir, ".cache/yarn"); | ||||
|     expect(resolvedPath).toBe(expectedPath); | ||||
| }); | ||||
| 
 | ||||
| test("resolvePath with home not found", () => { | ||||
|     const filePath = "~/.cache/yarn"; | ||||
|     const homedirMock = jest.spyOn(os, "homedir"); | ||||
|     homedirMock.mockImplementation(() => { | ||||
|         return ""; | ||||
|     }); | ||||
| 
 | ||||
|     expect(() => actionUtils.resolvePath(filePath)).toThrow( | ||||
|         "Unable to resolve `~` to HOME" | ||||
|     ); | ||||
| }); | ||||
| 
 | ||||
| test("isValidEvent returns true for push event", () => { | ||||
|     const event = Events.Push; | ||||
|     process.env[Events.Key] = event; | ||||
| 
 | ||||
|     const isValidEvent = actionUtils.isValidEvent(); | ||||
| 
 | ||||
|     expect(isValidEvent).toBe(true); | ||||
| }); | ||||
| 
 | ||||
| test("isValidEvent returns true for pull request event", () => { | ||||
|     const event = Events.PullRequest; | ||||
|     process.env[Events.Key] = event; | ||||
| 
 | ||||
|     const isValidEvent = actionUtils.isValidEvent(); | ||||
| 
 | ||||
|     expect(isValidEvent).toBe(true); | ||||
| }); | ||||
| @ -1,22 +0,0 @@ | ||||
| import * as core from "@actions/core"; | ||||
| 
 | ||||
| import { Inputs } from "../src/constants"; | ||||
| import run from "../src/restore"; | ||||
| import * as testUtils from "../src/utils/testUtils"; | ||||
| 
 | ||||
| test("restore with no path", async () => { | ||||
|     const failedMock = jest.spyOn(core, "setFailed"); | ||||
|     await run(); | ||||
|     expect(failedMock).toHaveBeenCalledWith( | ||||
|         "Input required and not supplied: path" | ||||
|     ); | ||||
| }); | ||||
| 
 | ||||
| test("restore with no key", async () => { | ||||
|     testUtils.setInput(Inputs.Path, "node_modules"); | ||||
|     const failedMock = jest.spyOn(core, "setFailed"); | ||||
|     await run(); | ||||
|     expect(failedMock).toHaveBeenCalledWith( | ||||
|         "Input required and not supplied: key" | ||||
|     ); | ||||
| }); | ||||
							
								
								
									
										450
									
								
								__tests__/restore.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										450
									
								
								__tests__/restore.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,450 @@ | ||||
| import * as core from "@actions/core"; | ||||
| import * as exec from "@actions/exec"; | ||||
| import * as io from "@actions/io"; | ||||
| import * as path from "path"; | ||||
| import * as cacheHttpClient from "../src/cacheHttpClient"; | ||||
| import { Events, Inputs } from "../src/constants"; | ||||
| import { ArtifactCacheEntry } from "../src/contracts"; | ||||
| import run from "../src/restore"; | ||||
| import * as actionUtils from "../src/utils/actionUtils"; | ||||
| import * as testUtils from "../src/utils/testUtils"; | ||||
| 
 | ||||
| jest.mock("@actions/exec"); | ||||
| jest.mock("@actions/io"); | ||||
| jest.mock("../src/utils/actionUtils"); | ||||
| jest.mock("../src/cacheHttpClient"); | ||||
| 
 | ||||
| beforeAll(() => { | ||||
|     jest.spyOn(actionUtils, "resolvePath").mockImplementation(filePath => { | ||||
|         return path.resolve(filePath); | ||||
|     }); | ||||
| 
 | ||||
|     jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation( | ||||
|         (key, cacheResult) => { | ||||
|             const actualUtils = jest.requireActual("../src/utils/actionUtils"); | ||||
|             return actualUtils.isExactKeyMatch(key, cacheResult); | ||||
|         } | ||||
|     ); | ||||
| 
 | ||||
|     jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => { | ||||
|         const actualUtils = jest.requireActual("../src/utils/actionUtils"); | ||||
|         return actualUtils.isValidEvent(); | ||||
|     }); | ||||
| 
 | ||||
|     jest.spyOn(actionUtils, "getSupportedEvents").mockImplementation(() => { | ||||
|         const actualUtils = jest.requireActual("../src/utils/actionUtils"); | ||||
|         return actualUtils.getSupportedEvents(); | ||||
|     }); | ||||
| 
 | ||||
|     jest.spyOn(io, "which").mockImplementation(tool => { | ||||
|         return Promise.resolve(tool); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| beforeEach(() => { | ||||
|     process.env[Events.Key] = Events.Push; | ||||
| }); | ||||
| 
 | ||||
| afterEach(() => { | ||||
|     testUtils.clearInputs(); | ||||
|     delete process.env[Events.Key]; | ||||
| }); | ||||
| 
 | ||||
| test("restore with invalid event", async () => { | ||||
|     const failedMock = jest.spyOn(core, "setFailed"); | ||||
|     const invalidEvent = "commit_comment"; | ||||
|     process.env[Events.Key] = invalidEvent; | ||||
|     await run(); | ||||
|     expect(failedMock).toHaveBeenCalledWith( | ||||
|         `Event Validation Error: The event type ${invalidEvent} is not supported. Only push, pull_request events are supported at this time.` | ||||
|     ); | ||||
| }); | ||||
| 
 | ||||
| test("restore with no path should fail", async () => { | ||||
|     const failedMock = jest.spyOn(core, "setFailed"); | ||||
|     await run(); | ||||
|     expect(failedMock).toHaveBeenCalledWith( | ||||
|         "Input required and not supplied: path" | ||||
|     ); | ||||
| }); | ||||
| 
 | ||||
| test("restore with no key", async () => { | ||||
|     testUtils.setInput(Inputs.Path, "node_modules"); | ||||
|     const failedMock = jest.spyOn(core, "setFailed"); | ||||
|     await run(); | ||||
|     expect(failedMock).toHaveBeenCalledWith( | ||||
|         "Input required and not supplied: key" | ||||
|     ); | ||||
| }); | ||||
| 
 | ||||
| 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("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.` | ||||
|     ); | ||||
| }); | ||||
| 
 | ||||
| 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("restore with no cache found", async () => { | ||||
|     const key = "node-test"; | ||||
|     testUtils.setInputs({ | ||||
|         path: "node_modules", | ||||
|         key | ||||
|     }); | ||||
| 
 | ||||
|     const infoMock = jest.spyOn(core, "info"); | ||||
|     const warningMock = jest.spyOn(core, "warning"); | ||||
|     const failedMock = jest.spyOn(core, "setFailed"); | ||||
|     const stateMock = jest.spyOn(core, "saveState"); | ||||
| 
 | ||||
|     const clientMock = jest.spyOn(cacheHttpClient, "getCacheEntry"); | ||||
|     clientMock.mockImplementation(() => { | ||||
|         return Promise.resolve(null); | ||||
|     }); | ||||
| 
 | ||||
|     await run(); | ||||
| 
 | ||||
|     expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); | ||||
|     expect(warningMock).toHaveBeenCalledTimes(0); | ||||
|     expect(failedMock).toHaveBeenCalledTimes(0); | ||||
| 
 | ||||
|     expect(infoMock).toHaveBeenCalledWith( | ||||
|         `Cache not found for input keys: ${key}.` | ||||
|     ); | ||||
| }); | ||||
| 
 | ||||
| test("restore with server error should fail", async () => { | ||||
|     const key = "node-test"; | ||||
|     testUtils.setInputs({ | ||||
|         path: "node_modules", | ||||
|         key | ||||
|     }); | ||||
| 
 | ||||
|     const warningMock = jest.spyOn(core, "warning"); | ||||
|     const failedMock = jest.spyOn(core, "setFailed"); | ||||
|     const stateMock = jest.spyOn(core, "saveState"); | ||||
| 
 | ||||
|     const clientMock = jest.spyOn(cacheHttpClient, "getCacheEntry"); | ||||
|     clientMock.mockImplementation(() => { | ||||
|         throw new Error("HTTP Error Occurred"); | ||||
|     }); | ||||
| 
 | ||||
|     const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput"); | ||||
| 
 | ||||
|     await run(); | ||||
| 
 | ||||
|     expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); | ||||
| 
 | ||||
|     expect(warningMock).toHaveBeenCalledTimes(1); | ||||
|     expect(warningMock).toHaveBeenCalledWith("HTTP Error Occurred"); | ||||
| 
 | ||||
|     expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); | ||||
|     expect(setCacheHitOutputMock).toHaveBeenCalledWith(false); | ||||
| 
 | ||||
|     expect(failedMock).toHaveBeenCalledTimes(0); | ||||
| }); | ||||
| 
 | ||||
| test("restore with restore keys and no cache found", async () => { | ||||
|     const key = "node-test"; | ||||
|     const restoreKey = "node-"; | ||||
|     testUtils.setInputs({ | ||||
|         path: "node_modules", | ||||
|         key, | ||||
|         restoreKeys: [restoreKey] | ||||
|     }); | ||||
| 
 | ||||
|     const infoMock = jest.spyOn(core, "info"); | ||||
|     const warningMock = jest.spyOn(core, "warning"); | ||||
|     const failedMock = jest.spyOn(core, "setFailed"); | ||||
|     const stateMock = jest.spyOn(core, "saveState"); | ||||
| 
 | ||||
|     const clientMock = jest.spyOn(cacheHttpClient, "getCacheEntry"); | ||||
|     clientMock.mockImplementation(() => { | ||||
|         return Promise.resolve(null); | ||||
|     }); | ||||
| 
 | ||||
|     await run(); | ||||
| 
 | ||||
|     expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); | ||||
|     expect(warningMock).toHaveBeenCalledTimes(0); | ||||
|     expect(failedMock).toHaveBeenCalledTimes(0); | ||||
| 
 | ||||
|     expect(infoMock).toHaveBeenCalledWith( | ||||
|         `Cache not found for input keys: ${key}, ${restoreKey}.` | ||||
|     ); | ||||
| }); | ||||
| 
 | ||||
| test("restore with cache found", async () => { | ||||
|     const key = "node-test"; | ||||
|     const cachePath = path.resolve("node_modules"); | ||||
|     testUtils.setInputs({ | ||||
|         path: "node_modules", | ||||
|         key | ||||
|     }); | ||||
| 
 | ||||
|     const infoMock = jest.spyOn(core, "info"); | ||||
|     const warningMock = jest.spyOn(core, "warning"); | ||||
|     const failedMock = jest.spyOn(core, "setFailed"); | ||||
|     const stateMock = jest.spyOn(core, "saveState"); | ||||
| 
 | ||||
|     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 createTempDirectoryMock = jest.spyOn( | ||||
|         actionUtils, | ||||
|         "createTempDirectory" | ||||
|     ); | ||||
|     createTempDirectoryMock.mockImplementation(() => { | ||||
|         return Promise.resolve(tempPath); | ||||
|     }); | ||||
| 
 | ||||
|     const archivePath = path.join(tempPath, "cache.tgz"); | ||||
|     const setCacheStateMock = jest.spyOn(actionUtils, "setCacheState"); | ||||
|     const downloadCacheMock = jest.spyOn(cacheHttpClient, "downloadCache"); | ||||
| 
 | ||||
|     const fileSize = 142; | ||||
|     const getArchiveFileSizeMock = jest | ||||
|         .spyOn(actionUtils, "getArchiveFileSize") | ||||
|         .mockReturnValue(fileSize); | ||||
| 
 | ||||
|     const mkdirMock = jest.spyOn(io, "mkdirP"); | ||||
|     const execMock = jest.spyOn(exec, "exec"); | ||||
|     const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput"); | ||||
| 
 | ||||
|     await run(); | ||||
| 
 | ||||
|     expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); | ||||
|     expect(getCacheMock).toHaveBeenCalledWith([key]); | ||||
|     expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry); | ||||
|     expect(createTempDirectoryMock).toHaveBeenCalledTimes(1); | ||||
|     expect(downloadCacheMock).toHaveBeenCalledWith(cacheEntry, archivePath); | ||||
|     expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath); | ||||
|     expect(mkdirMock).toHaveBeenCalledWith(cachePath); | ||||
| 
 | ||||
|     const IS_WINDOWS = process.platform === "win32"; | ||||
|     const args = IS_WINDOWS | ||||
|         ? [ | ||||
|               "-xz", | ||||
|               "--force-local", | ||||
|               "-f", | ||||
|               archivePath.replace(/\\/g, "/"), | ||||
|               "-C", | ||||
|               cachePath.replace(/\\/g, "/") | ||||
|           ] | ||||
|         : ["-xz", "-f", archivePath, "-C", cachePath]; | ||||
| 
 | ||||
|     expect(execMock).toHaveBeenCalledTimes(1); | ||||
|     expect(execMock).toHaveBeenCalledWith(`"tar"`, args); | ||||
| 
 | ||||
|     expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); | ||||
|     expect(setCacheHitOutputMock).toHaveBeenCalledWith(true); | ||||
| 
 | ||||
|     expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`); | ||||
|     expect(warningMock).toHaveBeenCalledTimes(0); | ||||
|     expect(failedMock).toHaveBeenCalledTimes(0); | ||||
| }); | ||||
| 
 | ||||
| test("restore with a pull request event and cache found", async () => { | ||||
|     const key = "node-test"; | ||||
|     const cachePath = path.resolve("node_modules"); | ||||
|     testUtils.setInputs({ | ||||
|         path: "node_modules", | ||||
|         key | ||||
|     }); | ||||
| 
 | ||||
|     process.env[Events.Key] = Events.PullRequest; | ||||
| 
 | ||||
|     const infoMock = jest.spyOn(core, "info"); | ||||
|     const warningMock = jest.spyOn(core, "warning"); | ||||
|     const failedMock = jest.spyOn(core, "setFailed"); | ||||
|     const stateMock = jest.spyOn(core, "saveState"); | ||||
| 
 | ||||
|     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 createTempDirectoryMock = jest.spyOn( | ||||
|         actionUtils, | ||||
|         "createTempDirectory" | ||||
|     ); | ||||
|     createTempDirectoryMock.mockImplementation(() => { | ||||
|         return Promise.resolve(tempPath); | ||||
|     }); | ||||
| 
 | ||||
|     const archivePath = path.join(tempPath, "cache.tgz"); | ||||
|     const setCacheStateMock = jest.spyOn(actionUtils, "setCacheState"); | ||||
|     const downloadCacheMock = jest.spyOn(cacheHttpClient, "downloadCache"); | ||||
| 
 | ||||
|     const fileSize = 62915000; | ||||
|     const getArchiveFileSizeMock = jest | ||||
|         .spyOn(actionUtils, "getArchiveFileSize") | ||||
|         .mockReturnValue(fileSize); | ||||
| 
 | ||||
|     const mkdirMock = jest.spyOn(io, "mkdirP"); | ||||
|     const execMock = jest.spyOn(exec, "exec"); | ||||
|     const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput"); | ||||
| 
 | ||||
|     await run(); | ||||
| 
 | ||||
|     expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); | ||||
|     expect(getCacheMock).toHaveBeenCalledWith([key]); | ||||
|     expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry); | ||||
|     expect(createTempDirectoryMock).toHaveBeenCalledTimes(1); | ||||
|     expect(downloadCacheMock).toHaveBeenCalledWith(cacheEntry, archivePath); | ||||
|     expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath); | ||||
|     expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~60 MB (62915000 B)`); | ||||
|     expect(mkdirMock).toHaveBeenCalledWith(cachePath); | ||||
| 
 | ||||
|     const IS_WINDOWS = process.platform === "win32"; | ||||
|     const args = IS_WINDOWS | ||||
|         ? [ | ||||
|               "-xz", | ||||
|               "--force-local", | ||||
|               "-f", | ||||
|               archivePath.replace(/\\/g, "/"), | ||||
|               "-C", | ||||
|               cachePath.replace(/\\/g, "/") | ||||
|           ] | ||||
|         : ["-xz", "-f", archivePath, "-C", cachePath]; | ||||
| 
 | ||||
|     expect(execMock).toHaveBeenCalledTimes(1); | ||||
|     expect(execMock).toHaveBeenCalledWith(`"tar"`, args); | ||||
| 
 | ||||
|     expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); | ||||
|     expect(setCacheHitOutputMock).toHaveBeenCalledWith(true); | ||||
| 
 | ||||
|     expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`); | ||||
|     expect(warningMock).toHaveBeenCalledTimes(0); | ||||
|     expect(failedMock).toHaveBeenCalledTimes(0); | ||||
| }); | ||||
| 
 | ||||
| test("restore with cache found for restore key", async () => { | ||||
|     const key = "node-test"; | ||||
|     const restoreKey = "node-"; | ||||
|     const cachePath = path.resolve("node_modules"); | ||||
|     testUtils.setInputs({ | ||||
|         path: "node_modules", | ||||
|         key, | ||||
|         restoreKeys: [restoreKey] | ||||
|     }); | ||||
| 
 | ||||
|     const infoMock = jest.spyOn(core, "info"); | ||||
|     const warningMock = jest.spyOn(core, "warning"); | ||||
|     const failedMock = jest.spyOn(core, "setFailed"); | ||||
|     const stateMock = jest.spyOn(core, "saveState"); | ||||
| 
 | ||||
|     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"; | ||||
| 
 | ||||
|     const createTempDirectoryMock = jest.spyOn( | ||||
|         actionUtils, | ||||
|         "createTempDirectory" | ||||
|     ); | ||||
|     createTempDirectoryMock.mockImplementation(() => { | ||||
|         return Promise.resolve(tempPath); | ||||
|     }); | ||||
| 
 | ||||
|     const archivePath = path.join(tempPath, "cache.tgz"); | ||||
|     const setCacheStateMock = jest.spyOn(actionUtils, "setCacheState"); | ||||
|     const downloadCacheMock = jest.spyOn(cacheHttpClient, "downloadCache"); | ||||
| 
 | ||||
|     const fileSize = 142; | ||||
|     const getArchiveFileSizeMock = jest | ||||
|         .spyOn(actionUtils, "getArchiveFileSize") | ||||
|         .mockReturnValue(fileSize); | ||||
| 
 | ||||
|     const mkdirMock = jest.spyOn(io, "mkdirP"); | ||||
|     const execMock = jest.spyOn(exec, "exec"); | ||||
|     const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput"); | ||||
| 
 | ||||
|     await run(); | ||||
| 
 | ||||
|     expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); | ||||
|     expect(getCacheMock).toHaveBeenCalledWith([key, restoreKey]); | ||||
|     expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry); | ||||
|     expect(createTempDirectoryMock).toHaveBeenCalledTimes(1); | ||||
|     expect(downloadCacheMock).toHaveBeenCalledWith(cacheEntry, archivePath); | ||||
|     expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath); | ||||
|     expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~0 MB (142 B)`); | ||||
|     expect(mkdirMock).toHaveBeenCalledWith(cachePath); | ||||
| 
 | ||||
|     const IS_WINDOWS = process.platform === "win32"; | ||||
|     const args = IS_WINDOWS | ||||
|         ? [ | ||||
|               "-xz", | ||||
|               "--force-local", | ||||
|               "-f", | ||||
|               archivePath.replace(/\\/g, "/"), | ||||
|               "-C", | ||||
|               cachePath.replace(/\\/g, "/") | ||||
|           ] | ||||
|         : ["-xz", "-f", archivePath, "-C", cachePath]; | ||||
| 
 | ||||
|     expect(execMock).toHaveBeenCalledTimes(1); | ||||
|     expect(execMock).toHaveBeenCalledWith(`"tar"`, args); | ||||
| 
 | ||||
|     expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); | ||||
|     expect(setCacheHitOutputMock).toHaveBeenCalledWith(false); | ||||
| 
 | ||||
|     expect(infoMock).toHaveBeenCalledWith( | ||||
|         `Cache restored from key: ${restoreKey}` | ||||
|     ); | ||||
|     expect(warningMock).toHaveBeenCalledTimes(0); | ||||
|     expect(failedMock).toHaveBeenCalledTimes(0); | ||||
| }); | ||||
							
								
								
									
										329
									
								
								__tests__/save.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										329
									
								
								__tests__/save.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,329 @@ | ||||
| import * as core from "@actions/core"; | ||||
| import * as exec from "@actions/exec"; | ||||
| import * as io from "@actions/io"; | ||||
| import * as path from "path"; | ||||
| import * as cacheHttpClient from "../src/cacheHttpClient"; | ||||
| import { Inputs } from "../src/constants"; | ||||
| import { ArtifactCacheEntry } from "../src/contracts"; | ||||
| import run from "../src/save"; | ||||
| import * as actionUtils from "../src/utils/actionUtils"; | ||||
| import * as testUtils from "../src/utils/testUtils"; | ||||
| 
 | ||||
| jest.mock("@actions/core"); | ||||
| jest.mock("@actions/exec"); | ||||
| jest.mock("@actions/io"); | ||||
| jest.mock("../src/utils/actionUtils"); | ||||
| jest.mock("../src/cacheHttpClient"); | ||||
| 
 | ||||
| beforeAll(() => { | ||||
|     jest.spyOn(core, "getInput").mockImplementation((name, options) => { | ||||
|         return jest.requireActual("@actions/core").getInput(name, options); | ||||
|     }); | ||||
| 
 | ||||
|     jest.spyOn(actionUtils, "getCacheState").mockImplementation(() => { | ||||
|         return jest.requireActual("../src/utils/actionUtils").getCacheState(); | ||||
|     }); | ||||
| 
 | ||||
|     jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation( | ||||
|         (key, cacheResult) => { | ||||
|             return jest | ||||
|                 .requireActual("../src/utils/actionUtils") | ||||
|                 .isExactKeyMatch(key, cacheResult); | ||||
|         } | ||||
|     ); | ||||
| 
 | ||||
|     jest.spyOn(actionUtils, "resolvePath").mockImplementation(filePath => { | ||||
|         return path.resolve(filePath); | ||||
|     }); | ||||
| 
 | ||||
|     jest.spyOn(actionUtils, "createTempDirectory").mockImplementation(() => { | ||||
|         return Promise.resolve("/foo/bar"); | ||||
|     }); | ||||
| 
 | ||||
|     jest.spyOn(io, "which").mockImplementation(tool => { | ||||
|         return Promise.resolve(tool); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| afterEach(() => { | ||||
|     testUtils.clearInputs(); | ||||
| }); | ||||
| 
 | ||||
| test("save with no primary key in state outputs warning", async () => { | ||||
|     const warningMock = jest.spyOn(core, "warning"); | ||||
|     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(warningMock).toHaveBeenCalledWith( | ||||
|         `Error retrieving key from state.` | ||||
|     ); | ||||
|     expect(warningMock).toHaveBeenCalledTimes(1); | ||||
|     expect(failedMock).toHaveBeenCalledTimes(0); | ||||
| }); | ||||
| 
 | ||||
| test("save with exact match returns early", async () => { | ||||
|     const infoMock = jest.spyOn(core, "info"); | ||||
|     const warningMock = jest.spyOn(core, "warning"); | ||||
|     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 execMock = jest.spyOn(exec, "exec"); | ||||
| 
 | ||||
|     await run(); | ||||
| 
 | ||||
|     expect(infoMock).toHaveBeenCalledWith( | ||||
|         `Cache hit occurred on the primary key ${primaryKey}, not saving cache.` | ||||
|     ); | ||||
| 
 | ||||
|     expect(execMock).toHaveBeenCalledTimes(0); | ||||
| 
 | ||||
|     expect(warningMock).toHaveBeenCalledTimes(0); | ||||
|     expect(failedMock).toHaveBeenCalledTimes(0); | ||||
| }); | ||||
| 
 | ||||
| test("save with missing input outputs warning", async () => { | ||||
|     const warningMock = jest.spyOn(core, "warning"); | ||||
|     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(warningMock).toHaveBeenCalledWith( | ||||
|         "Input required and not supplied: path" | ||||
|     ); | ||||
|     expect(warningMock).toHaveBeenCalledTimes(1); | ||||
|     expect(failedMock).toHaveBeenCalledTimes(0); | ||||
| }); | ||||
| 
 | ||||
| test("save with large cache outputs warning", async () => { | ||||
|     const warningMock = jest.spyOn(core, "warning"); | ||||
|     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 cachePath = path.resolve(inputPath); | ||||
|     testUtils.setInput(Inputs.Path, inputPath); | ||||
| 
 | ||||
|     const execMock = jest.spyOn(exec, "exec"); | ||||
| 
 | ||||
|     const cacheSize = 1024 * 1024 * 1024; //~1GB, over the 400MB limit
 | ||||
|     jest.spyOn(actionUtils, "getArchiveFileSize").mockImplementationOnce(() => { | ||||
|         return cacheSize; | ||||
|     }); | ||||
| 
 | ||||
|     await run(); | ||||
| 
 | ||||
|     const archivePath = path.join("/foo/bar", "cache.tgz"); | ||||
| 
 | ||||
|     const IS_WINDOWS = process.platform === "win32"; | ||||
|     const args = IS_WINDOWS | ||||
|         ? [ | ||||
|               "-cz", | ||||
|               "--force-local", | ||||
|               "-f", | ||||
|               archivePath.replace(/\\/g, "/"), | ||||
|               "-C", | ||||
|               cachePath.replace(/\\/g, "/"), | ||||
|               "." | ||||
|           ] | ||||
|         : ["-cz", "-f", archivePath, "-C", cachePath, "."]; | ||||
| 
 | ||||
|     expect(execMock).toHaveBeenCalledTimes(1); | ||||
|     expect(execMock).toHaveBeenCalledWith(`"tar"`, args); | ||||
| 
 | ||||
|     expect(warningMock).toHaveBeenCalledTimes(1); | ||||
|     expect(warningMock).toHaveBeenCalledWith( | ||||
|         "Cache size of ~1024 MB (1073741824 B) is over the 400MB limit, not saving cache." | ||||
|     ); | ||||
| 
 | ||||
|     expect(failedMock).toHaveBeenCalledTimes(0); | ||||
| }); | ||||
| 
 | ||||
| test("save with server error outputs warning", async () => { | ||||
|     const warningMock = jest.spyOn(core, "warning"); | ||||
|     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 cachePath = path.resolve(inputPath); | ||||
|     testUtils.setInput(Inputs.Path, inputPath); | ||||
| 
 | ||||
|     const execMock = jest.spyOn(exec, "exec"); | ||||
| 
 | ||||
|     const saveCacheMock = jest | ||||
|         .spyOn(cacheHttpClient, "saveCache") | ||||
|         .mockImplementationOnce(() => { | ||||
|             throw new Error("HTTP Error Occurred"); | ||||
|         }); | ||||
| 
 | ||||
|     await run(); | ||||
| 
 | ||||
|     const archivePath = path.join("/foo/bar", "cache.tgz"); | ||||
| 
 | ||||
|     const IS_WINDOWS = process.platform === "win32"; | ||||
|     const args = IS_WINDOWS | ||||
|         ? [ | ||||
|               "-cz", | ||||
|               "--force-local", | ||||
|               "-f", | ||||
|               archivePath.replace(/\\/g, "/"), | ||||
|               "-C", | ||||
|               cachePath.replace(/\\/g, "/"), | ||||
|               "." | ||||
|           ] | ||||
|         : ["-cz", "-f", archivePath, "-C", cachePath, "."]; | ||||
| 
 | ||||
|     expect(execMock).toHaveBeenCalledTimes(1); | ||||
|     expect(execMock).toHaveBeenCalledWith(`"tar"`, args); | ||||
| 
 | ||||
|     expect(saveCacheMock).toHaveBeenCalledTimes(1); | ||||
|     expect(saveCacheMock).toHaveBeenCalledWith(primaryKey, archivePath); | ||||
| 
 | ||||
|     expect(warningMock).toHaveBeenCalledTimes(1); | ||||
|     expect(warningMock).toHaveBeenCalledWith("HTTP Error Occurred"); | ||||
| 
 | ||||
|     expect(failedMock).toHaveBeenCalledTimes(0); | ||||
| }); | ||||
| 
 | ||||
| test("save with valid inputs uploads a cache", async () => { | ||||
|     const warningMock = jest.spyOn(core, "warning"); | ||||
|     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 cachePath = path.resolve(inputPath); | ||||
|     testUtils.setInput(Inputs.Path, inputPath); | ||||
| 
 | ||||
|     const execMock = jest.spyOn(exec, "exec"); | ||||
| 
 | ||||
|     const saveCacheMock = jest.spyOn(cacheHttpClient, "saveCache"); | ||||
| 
 | ||||
|     await run(); | ||||
| 
 | ||||
|     const archivePath = path.join("/foo/bar", "cache.tgz"); | ||||
| 
 | ||||
|     const IS_WINDOWS = process.platform === "win32"; | ||||
|     const args = IS_WINDOWS | ||||
|         ? [ | ||||
|               "-cz", | ||||
|               "--force-local", | ||||
|               "-f", | ||||
|               archivePath.replace(/\\/g, "/"), | ||||
|               "-C", | ||||
|               cachePath.replace(/\\/g, "/"), | ||||
|               "." | ||||
|           ] | ||||
|         : ["-cz", "-f", archivePath, "-C", cachePath, "."]; | ||||
| 
 | ||||
|     expect(execMock).toHaveBeenCalledTimes(1); | ||||
|     expect(execMock).toHaveBeenCalledWith(`"tar"`, args); | ||||
| 
 | ||||
|     expect(saveCacheMock).toHaveBeenCalledTimes(1); | ||||
|     expect(saveCacheMock).toHaveBeenCalledWith(primaryKey, archivePath); | ||||
| 
 | ||||
|     expect(warningMock).toHaveBeenCalledTimes(0); | ||||
|     expect(failedMock).toHaveBeenCalledTimes(0); | ||||
| }); | ||||
							
								
								
									
										135
									
								
								examples.md
									
									
									
									
									
								
							
							
						
						
									
										135
									
								
								examples.md
									
									
									
									
									
								
							| @ -1,12 +1,21 @@ | ||||
| # Examples | ||||
| 
 | ||||
| - [Examples](#examples) | ||||
|   - [C# - Nuget](#c---nuget) | ||||
|   - [Elixir - Mix](#elixir---mix) | ||||
|   - [Go - Modules](#go---modules) | ||||
|   - [Java - Gradle](#java---gradle) | ||||
|   - [Java - Maven](#java---maven) | ||||
|   - [Node - npm](#node---npm) | ||||
|     - [macOS and Ubuntu](#macos-and-ubuntu) | ||||
|     - [Windows](#windows) | ||||
|     - [Using multiple systems and `npm config`](#using-multiple-systems-and-npm-config) | ||||
|   - [Node - Yarn](#node---yarn) | ||||
|   - [PHP - Composer](#php---composer) | ||||
|   - [Python - pip](#python---pip) | ||||
|     - [Simple example](#simple-example) | ||||
|     - [Multiple OS's in a workflow](#multiple-oss-in-a-workflow) | ||||
|     - [Using a script to get cache location](#using-a-script-to-get-cache-location) | ||||
|   - [Ruby - Gem](#ruby---gem) | ||||
|   - [Rust - Cargo](#rust---cargo) | ||||
|   - [Swift, Objective-C - Carthage](#swift-objective-c---carthage) | ||||
| @ -69,24 +78,142 @@ Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/packa | ||||
| 
 | ||||
| ## Node - npm | ||||
| 
 | ||||
| For npm, cache files are stored in `~/.npm` on Posix, or `%AppData%/npm-cache` on Windows. See https://docs.npmjs.com/cli/cache#cache | ||||
| 
 | ||||
| >Note: It is not recommended to cache `node_modules`, as it can break across Node versions and won't work with `npm ci` | ||||
| 
 | ||||
| ### macOS and Ubuntu | ||||
| 
 | ||||
| ```yaml | ||||
| - uses: actions/cache@v1 | ||||
|   with: | ||||
|     path: node_modules | ||||
|     path: ~/.npm | ||||
|     key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} | ||||
|     restore-keys: | | ||||
|       ${{ runner.os }}-node- | ||||
| ``` | ||||
| 
 | ||||
| ### Windows | ||||
| 
 | ||||
| ```yaml | ||||
| - uses: actions/cache@v1 | ||||
|   with: | ||||
|     path: ~\AppData\Roaming\npm-cache | ||||
|     key: ${{ runner.os }}-node-${{ hashFiles('**\package-lock.json') }} | ||||
|     restore-keys: | | ||||
|       ${{ runner.os }}-node- | ||||
| ``` | ||||
| 
 | ||||
| ### Using multiple systems and `npm config` | ||||
| 
 | ||||
| ```yaml   | ||||
| - name: Get npm cache directory | ||||
|   id: npm-cache | ||||
|   run: | | ||||
|     echo "::set-output name=dir::$(npm config get cache)" | ||||
| - uses: actions/cache@v1 | ||||
|   with: | ||||
|     path: ${{ steps.npm-cache.outputs.dir }} | ||||
|     key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} | ||||
|     restore-keys: | | ||||
|       ${{ runner.os }}-node- | ||||
| ``` | ||||
| 
 | ||||
| ## Node - Yarn | ||||
| The yarn cache directory will depend on your operating system and version of `yarn`. See https://yarnpkg.com/lang/en/docs/cli/cache/ for more info. | ||||
| 
 | ||||
| ```yaml | ||||
| - name: Get yarn cache | ||||
|   id: yarn-cache | ||||
|   run: echo "::set-output name=dir::$(yarn cache dir)" | ||||
| 
 | ||||
| - uses: actions/cache@v1 | ||||
|   with: | ||||
|     path: ${{ steps.yarn-cache.outputs.dir }} | ||||
|     key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} | ||||
|     restore-keys: | | ||||
|       ${{ runner.os }}-yarn- | ||||
| ``` | ||||
| 
 | ||||
| ## PHP - Composer | ||||
| 
 | ||||
| ```yaml   | ||||
| - name: Get Composer Cache Directory | ||||
|   id: composer-cache | ||||
|   run: | | ||||
|     echo "::set-output name=dir::$(composer config cache-files-dir)" | ||||
| - uses: actions/cache@v1 | ||||
|   with: | ||||
|     path: ${{ steps.composer-cache.outputs.dir }} | ||||
|     key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} | ||||
|     restore-keys: | | ||||
|       ${{ runner.os }}-composer- | ||||
| ``` | ||||
| 
 | ||||
| ## Python - pip | ||||
| 
 | ||||
| For pip, the cache directory will vary by OS. See https://pip.pypa.io/en/stable/reference/pip_install/#caching | ||||
| 
 | ||||
| Locations: | ||||
|  - Ubuntu: `~/.cache/pip` | ||||
|  - Windows: `~\AppData\Local\pip\Cache` | ||||
|  - macOS: `~/Library/Caches/pip` | ||||
| 
 | ||||
| ### Simple example | ||||
| ```yaml | ||||
| - uses: actions/cache@v1 | ||||
|   with: | ||||
|     path: ~/.cache/yarn | ||||
|     key: ${{ runner.os }}-yarn-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }} | ||||
|     path: ~/.cache/pip | ||||
|     key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} | ||||
|     restore-keys: | | ||||
|       ${{ runner.os }}-yarn- | ||||
|       ${{ runner.os }}-pip- | ||||
| ``` | ||||
| 
 | ||||
| Replace `~/.cache/pip` with the correct `path` if not using Ubuntu. | ||||
| 
 | ||||
| ### Multiple OS's in a workflow | ||||
| 
 | ||||
| ```yaml | ||||
| - uses: actions/cache@v1 | ||||
|   if: startsWith(runner.os, 'Linux') | ||||
|   with: | ||||
|     path: ~/.cache/pip | ||||
|     key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} | ||||
|     restore-keys: | | ||||
|       ${{ runner.os }}-pip- | ||||
| 
 | ||||
| - uses: actions/cache@v1 | ||||
|   if: startsWith(runner.os, 'macOS') | ||||
|   with: | ||||
|     path: ~/Library/Caches/pip | ||||
|     key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} | ||||
|     restore-keys: | | ||||
|       ${{ runner.os }}-pip- | ||||
| 
 | ||||
| - uses: actions/cache@v1 | ||||
|   if: startsWith(runner.os, 'Windows') | ||||
|   with: | ||||
|     path: ~\AppData\Local\pip\Cache | ||||
|     key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} | ||||
|     restore-keys: | | ||||
|       ${{ runner.os }}-pip- | ||||
| ``` | ||||
| 
 | ||||
| ### Using a script to get cache location | ||||
| 
 | ||||
| > Note: This uses an internal pip API and may not always work | ||||
| ```yaml | ||||
| - name: Get pip cache | ||||
|    id: pip-cache | ||||
|    run: | | ||||
|      python -c "from pip._internal.locations import USER_CACHE_DIR; print('::set-output name=dir::' + USER_CACHE_DIR)" | ||||
| 
 | ||||
| - uses: actions/cache@v1 | ||||
|   with: | ||||
|     path: ${{ steps.pip-cache.outputs.dir }} | ||||
|     key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} | ||||
|     restore-keys: | | ||||
|       ${{ runner.os }}-pip- | ||||
| ``` | ||||
| 
 | ||||
| ## Ruby - Gem | ||||
|  | ||||
| @ -1,20 +1,23 @@ | ||||
| require("nock").disableNetConnect(); | ||||
| 
 | ||||
| module.exports = { | ||||
|     clearMocks: true, | ||||
|   moduleFileExtensions: ['js', 'ts'], | ||||
|   testEnvironment: 'node', | ||||
|   testMatch: ['**/*.test.ts'], | ||||
|   testRunner: 'jest-circus/runner', | ||||
|     moduleFileExtensions: ["js", "ts"], | ||||
|     testEnvironment: "node", | ||||
|     testMatch: ["**/*.test.ts"], | ||||
|     testRunner: "jest-circus/runner", | ||||
|     transform: { | ||||
|     '^.+\\.ts$': 'ts-jest' | ||||
|         "^.+\\.ts$": "ts-jest" | ||||
|     }, | ||||
|     verbose: true | ||||
| } | ||||
| }; | ||||
| 
 | ||||
| const processStdoutWrite = process.stdout.write.bind(process.stdout) | ||||
| const processStdoutWrite = process.stdout.write.bind(process.stdout); | ||||
| // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
 | ||||
| process.stdout.write = (str, encoding, cb) => { | ||||
|     // Core library will directly call process.stdout.write for commands
 | ||||
|     // We don't want :: commands to be executed by the runner during tests
 | ||||
|     if (!str.match(/^::/)) { | ||||
|         return processStdoutWrite(str, encoding, cb); | ||||
|     } | ||||
| } | ||||
| }; | ||||
|  | ||||
							
								
								
									
										1023
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1023
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							| @ -7,6 +7,7 @@ | ||||
|   "scripts": { | ||||
|     "build": "tsc", | ||||
|     "test": "tsc --noEmit && jest --coverage", | ||||
|     "lint": "eslint **/*.ts --cache", | ||||
|     "format": "prettier --write **/*.ts", | ||||
|     "format-check": "prettier --check **/*.ts", | ||||
|     "release": "ncc build -o dist/restore src/restore.ts && ncc build -o dist/save src/save.ts && git add -f dist/" | ||||
| @ -31,11 +32,20 @@ | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@types/jest": "^24.0.13", | ||||
|     "@types/nock": "^11.1.0", | ||||
|     "@types/node": "^12.0.4", | ||||
|     "@types/uuid": "^3.4.5", | ||||
|     "@typescript-eslint/eslint-plugin": "^2.7.0", | ||||
|     "@typescript-eslint/parser": "^2.7.0", | ||||
|     "@zeit/ncc": "^0.20.5", | ||||
|     "eslint": "^6.6.0", | ||||
|     "eslint-config-prettier": "^6.5.0", | ||||
|     "eslint-plugin-import": "^2.18.2", | ||||
|     "eslint-plugin-jest": "^23.0.3", | ||||
|     "eslint-plugin-prettier": "^3.1.1", | ||||
|     "jest": "^24.8.0", | ||||
|     "jest-circus": "^24.7.1", | ||||
|     "nock": "^11.7.0", | ||||
|     "prettier": "1.18.2", | ||||
|     "ts-jest": "^24.0.2", | ||||
|     "typescript": "^3.6.4" | ||||
|  | ||||
| @ -1,13 +1,40 @@ | ||||
| import * as core from "@actions/core"; | ||||
| import * as fs from "fs"; | ||||
| 
 | ||||
| import { BearerCredentialHandler } from "typed-rest-client/Handlers"; | ||||
| import { HttpClient } from "typed-rest-client/HttpClient"; | ||||
| import { IHttpClientResponse } from "typed-rest-client/Interfaces"; | ||||
| import { RestClient, IRequestOptions } from "typed-rest-client/RestClient"; | ||||
| 
 | ||||
| import { IRequestOptions, RestClient } from "typed-rest-client/RestClient"; | ||||
| import { ArtifactCacheEntry } from "./contracts"; | ||||
| 
 | ||||
| function getCacheUrl(): string { | ||||
|     // Ideally we just use ACTIONS_CACHE_URL
 | ||||
|     const cacheUrl: string = ( | ||||
|         process.env["ACTIONS_CACHE_URL"] || | ||||
|         process.env["ACTIONS_RUNTIME_URL"] || | ||||
|         "" | ||||
|     ).replace("pipelines", "artifactcache"); | ||||
|     if (!cacheUrl) { | ||||
|         throw new Error( | ||||
|             "Cache Service Url not found, unable to restore cache." | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     core.debug(`Cache Url: ${cacheUrl}`); | ||||
|     return cacheUrl; | ||||
| } | ||||
| 
 | ||||
| function createAcceptHeader(type: string, apiVersion: string): string { | ||||
|     return `${type};api-version=${apiVersion}`; | ||||
| } | ||||
| 
 | ||||
| function getRequestOptions(): IRequestOptions { | ||||
|     const requestOptions: IRequestOptions = { | ||||
|         acceptHeader: createAcceptHeader("application/json", "5.2-preview.1") | ||||
|     }; | ||||
| 
 | ||||
|     return requestOptions; | ||||
| } | ||||
| 
 | ||||
| export async function getCacheEntry( | ||||
|     keys: string[] | ||||
| ): Promise<ArtifactCacheEntry | null> { | ||||
| @ -43,16 +70,6 @@ export async function getCacheEntry( | ||||
|     return cacheResult; | ||||
| } | ||||
| 
 | ||||
| export async function downloadCache( | ||||
|     cacheEntry: ArtifactCacheEntry, | ||||
|     archivePath: string | ||||
| ): Promise<void> { | ||||
|     const stream = fs.createWriteStream(archivePath); | ||||
|     const httpClient = new HttpClient("actions/cache"); | ||||
|     const downloadResponse = await httpClient.get(cacheEntry.archiveLocation!); | ||||
|     await pipeResponseToStream(downloadResponse, stream); | ||||
| } | ||||
| 
 | ||||
| async function pipeResponseToStream( | ||||
|     response: IHttpClientResponse, | ||||
|     stream: NodeJS.WritableStream | ||||
| @ -64,7 +81,23 @@ async function pipeResponseToStream( | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| export async function saveCache(stream: NodeJS.ReadableStream, key: string) { | ||||
| export async function downloadCache( | ||||
|     cacheEntry: ArtifactCacheEntry, | ||||
|     archivePath: string | ||||
| ): Promise<void> { | ||||
|     const stream = fs.createWriteStream(archivePath); | ||||
|     const httpClient = new HttpClient("actions/cache"); | ||||
|     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
 | ||||
|     const downloadResponse = await httpClient.get(cacheEntry.archiveLocation!); | ||||
|     await pipeResponseToStream(downloadResponse, stream); | ||||
| } | ||||
| 
 | ||||
| export async function saveCache( | ||||
|     key: string, | ||||
|     archivePath: string | ||||
| ): Promise<void> { | ||||
|     const stream = fs.createReadStream(archivePath); | ||||
| 
 | ||||
|     const cacheUrl = getCacheUrl(); | ||||
|     const token = process.env["ACTIONS_RUNTIME_TOKEN"] || ""; | ||||
|     const bearerCredentialHandler = new BearerCredentialHandler(token); | ||||
| @ -93,32 +126,3 @@ export async function saveCache(stream: NodeJS.ReadableStream, key: string) { | ||||
| 
 | ||||
|     core.info("Cache saved successfully"); | ||||
| } | ||||
| 
 | ||||
| function getRequestOptions(): IRequestOptions { | ||||
|     const requestOptions: IRequestOptions = { | ||||
|         acceptHeader: createAcceptHeader("application/json", "5.2-preview.1") | ||||
|     }; | ||||
| 
 | ||||
|     return requestOptions; | ||||
| } | ||||
| 
 | ||||
| function createAcceptHeader(type: string, apiVersion: string): string { | ||||
|     return `${type};api-version=${apiVersion}`; | ||||
| } | ||||
| 
 | ||||
| function getCacheUrl(): string { | ||||
|     // Ideally we just use ACTIONS_CACHE_URL
 | ||||
|     let cacheUrl: string = ( | ||||
|         process.env["ACTIONS_CACHE_URL"] || | ||||
|         process.env["ACTIONS_RUNTIME_URL"] || | ||||
|         "" | ||||
|     ).replace("pipelines", "artifactcache"); | ||||
|     if (!cacheUrl) { | ||||
|         throw new Error( | ||||
|             "Cache Service Url not found, unable to restore cache." | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     core.debug(`Cache Url: ${cacheUrl}`); | ||||
|     return cacheUrl; | ||||
| } | ||||
|  | ||||
| @ -1,14 +1,20 @@ | ||||
| export namespace Inputs { | ||||
|     export const Key = "key"; | ||||
|     export const Path = "path"; | ||||
|     export const RestoreKeys = "restore-keys"; | ||||
| export enum Inputs { | ||||
|     Key = "key", | ||||
|     Path = "path", | ||||
|     RestoreKeys = "restore-keys" | ||||
| } | ||||
| 
 | ||||
| export namespace Outputs { | ||||
|     export const CacheHit = "cache-hit"; | ||||
| export enum Outputs { | ||||
|     CacheHit = "cache-hit" | ||||
| } | ||||
| 
 | ||||
| export namespace State { | ||||
|     export const CacheKey = "CACHE_KEY"; | ||||
|     export const CacheResult = "CACHE_RESULT"; | ||||
| export enum State { | ||||
|     CacheKey = "CACHE_KEY", | ||||
|     CacheResult = "CACHE_RESULT" | ||||
| } | ||||
| 
 | ||||
| export enum Events { | ||||
|     Key = "GITHUB_EVENT_NAME", | ||||
|     Push = "push", | ||||
|     PullRequest = "pull_request" | ||||
| } | ||||
|  | ||||
| @ -1,18 +1,25 @@ | ||||
| import * as core from "@actions/core"; | ||||
| import { exec } from "@actions/exec"; | ||||
| import * as io from "@actions/io"; | ||||
| 
 | ||||
| import * as fs from "fs"; | ||||
| import * as path from "path"; | ||||
| 
 | ||||
| import * as cacheHttpClient from "./cacheHttpClient"; | ||||
| import { Inputs, State } from "./constants"; | ||||
| import { Events, Inputs, State } from "./constants"; | ||||
| import * as utils from "./utils/actionUtils"; | ||||
| 
 | ||||
| async function run() { | ||||
| async function run(): Promise<void> { | ||||
|     try { | ||||
|         // Validate inputs, this can cause task failure
 | ||||
|         let cachePath = utils.resolvePath( | ||||
|         if (!utils.isValidEvent()) { | ||||
|             core.setFailed( | ||||
|                 `Event Validation Error: The event type ${ | ||||
|                     process.env[Events.Key] | ||||
|                 } is not supported. Only ${utils | ||||
|                     .getSupportedEvents() | ||||
|                     .join(", ")} events are supported at this time.` | ||||
|             ); | ||||
|         } | ||||
| 
 | ||||
|         const cachePath = utils.resolvePath( | ||||
|             core.getInput(Inputs.Path, { required: true }) | ||||
|         ); | ||||
|         core.debug(`Cache Path: ${cachePath}`); | ||||
| @ -60,7 +67,7 @@ async function run() { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             let archivePath = path.join( | ||||
|             const archivePath = path.join( | ||||
|                 await utils.createTempDirectory(), | ||||
|                 "cache.tgz" | ||||
|             ); | ||||
| @ -72,26 +79,33 @@ async function run() { | ||||
|             // Download the cache from the cache entry
 | ||||
|             await cacheHttpClient.downloadCache(cacheEntry, archivePath); | ||||
| 
 | ||||
|             io.mkdirP(cachePath); | ||||
|             const archiveFileSize = utils.getArchiveFileSize(archivePath); | ||||
|             core.info( | ||||
|                 `Cache Size: ~${Math.round( | ||||
|                     archiveFileSize / (1024 * 1024) | ||||
|                 )} MB (${archiveFileSize} B)` | ||||
|             ); | ||||
| 
 | ||||
|             // Create directory to extract tar into
 | ||||
|             await io.mkdirP(cachePath); | ||||
| 
 | ||||
|             // http://man7.org/linux/man-pages/man1/tar.1.html
 | ||||
|             // tar [-options] <name of the tar archive> [files or directories which to add into archive]
 | ||||
|             const args = ["-xz"]; | ||||
| 
 | ||||
|             const IS_WINDOWS = process.platform === "win32"; | ||||
|             if (IS_WINDOWS) { | ||||
|                 args.push("--force-local"); | ||||
|                 archivePath = archivePath.replace(/\\/g, "/"); | ||||
|                 cachePath = cachePath.replace(/\\/g, "/"); | ||||
|             } | ||||
|             args.push(...["-f", archivePath, "-C", cachePath]); | ||||
|             const args = IS_WINDOWS | ||||
|                 ? [ | ||||
|                       "-xz", | ||||
|                       "--force-local", | ||||
|                       "-f", | ||||
|                       archivePath.replace(/\\/g, "/"), | ||||
|                       "-C", | ||||
|                       cachePath.replace(/\\/g, "/") | ||||
|                   ] | ||||
|                 : ["-xz", "-f", archivePath, "-C", cachePath]; | ||||
| 
 | ||||
|             const tarPath = await io.which("tar", true); | ||||
|             core.debug(`Tar Path: ${tarPath}`); | ||||
| 
 | ||||
|             const archiveFileSize = fs.statSync(archivePath).size; | ||||
|             core.debug(`File Size: ${archiveFileSize}`); | ||||
| 
 | ||||
|             await exec(`"${tarPath}"`, args); | ||||
| 
 | ||||
|             const isExactKeyMatch = utils.isExactKeyMatch( | ||||
|  | ||||
							
								
								
									
										37
									
								
								src/save.ts
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								src/save.ts
									
									
									
									
									
								
							| @ -1,15 +1,12 @@ | ||||
| import * as core from "@actions/core"; | ||||
| import { exec } from "@actions/exec"; | ||||
| 
 | ||||
| import * as io from "@actions/io"; | ||||
| import * as fs from "fs"; | ||||
| import * as path from "path"; | ||||
| 
 | ||||
| import * as cacheHttpClient from "./cacheHttpClient"; | ||||
| import { Inputs, State } from "./constants"; | ||||
| import * as utils from "./utils/actionUtils"; | ||||
| 
 | ||||
| async function run() { | ||||
| async function run(): Promise<void> { | ||||
|     try { | ||||
|         const state = utils.getCacheState(); | ||||
| 
 | ||||
| @ -27,12 +24,12 @@ async function run() { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         let cachePath = utils.resolvePath( | ||||
|         const cachePath = utils.resolvePath( | ||||
|             core.getInput(Inputs.Path, { required: true }) | ||||
|         ); | ||||
|         core.debug(`Cache Path: ${cachePath}`); | ||||
| 
 | ||||
|         let archivePath = path.join( | ||||
|         const archivePath = path.join( | ||||
|             await utils.createTempDirectory(), | ||||
|             "cache.tgz" | ||||
|         ); | ||||
| @ -40,32 +37,36 @@ async function run() { | ||||
| 
 | ||||
|         // http://man7.org/linux/man-pages/man1/tar.1.html
 | ||||
|         // tar [-options] <name of the tar archive> [files or directories which to add into archive]
 | ||||
|         const args = ["-cz"]; | ||||
|         const IS_WINDOWS = process.platform === "win32"; | ||||
|         if (IS_WINDOWS) { | ||||
|             args.push("--force-local"); | ||||
|             archivePath = archivePath.replace(/\\/g, "/"); | ||||
|             cachePath = cachePath.replace(/\\/g, "/"); | ||||
|         } | ||||
| 
 | ||||
|         args.push(...["-f", archivePath, "-C", cachePath, "."]); | ||||
|         const args = IS_WINDOWS | ||||
|             ? [ | ||||
|                   "-cz", | ||||
|                   "--force-local", | ||||
|                   "-f", | ||||
|                   archivePath.replace(/\\/g, "/"), | ||||
|                   "-C", | ||||
|                   cachePath.replace(/\\/g, "/"), | ||||
|                   "." | ||||
|               ] | ||||
|             : ["-cz", "-f", archivePath, "-C", cachePath, "."]; | ||||
| 
 | ||||
|         const tarPath = await io.which("tar", true); | ||||
|         core.debug(`Tar Path: ${tarPath}`); | ||||
|         await exec(`"${tarPath}"`, args); | ||||
| 
 | ||||
|         const fileSizeLimit = 400 * 1024 * 1024; // 400MB
 | ||||
|         const archiveFileSize = fs.statSync(archivePath).size; | ||||
|         const archiveFileSize = utils.getArchiveFileSize(archivePath); | ||||
|         core.debug(`File Size: ${archiveFileSize}`); | ||||
|         if (archiveFileSize > fileSizeLimit) { | ||||
|             core.warning( | ||||
|                 `Cache size of ${archiveFileSize} bytes is over the 400MB limit, not saving cache.` | ||||
|                 `Cache size of ~${Math.round( | ||||
|                     archiveFileSize / (1024 * 1024) | ||||
|                 )} MB (${archiveFileSize} B) is over the 400MB limit, not saving cache.` | ||||
|             ); | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const stream = fs.createReadStream(archivePath); | ||||
|         await cacheHttpClient.saveCache(stream, primaryKey); | ||||
|         await cacheHttpClient.saveCache(primaryKey, archivePath); | ||||
|     } catch (error) { | ||||
|         core.warning(error.message); | ||||
|     } | ||||
|  | ||||
| @ -1,10 +1,11 @@ | ||||
| import * as core from "@actions/core"; | ||||
| import * as io from "@actions/io"; | ||||
| import * as fs from "fs"; | ||||
| import * as os from "os"; | ||||
| import * as path from "path"; | ||||
| import * as uuidV4 from "uuid/v4"; | ||||
| 
 | ||||
| import { Outputs, State } from "../constants"; | ||||
| import { Events, Outputs, State } from "../constants"; | ||||
| import { ArtifactCacheEntry } from "../contracts"; | ||||
| 
 | ||||
| // From https://github.com/actions/toolkit/blob/master/packages/tool-cache/src/tool-cache.ts#L23
 | ||||
| @ -32,6 +33,10 @@ export async function createTempDirectory(): Promise<string> { | ||||
|     return dest; | ||||
| } | ||||
| 
 | ||||
| export function getArchiveFileSize(path: string): number { | ||||
|     return fs.statSync(path).size; | ||||
| } | ||||
| 
 | ||||
| export function isExactKeyMatch( | ||||
|     key: string, | ||||
|     cacheResult?: ArtifactCacheEntry | ||||
| @ -45,10 +50,18 @@ export function isExactKeyMatch( | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| export function setCacheState(state: ArtifactCacheEntry): void { | ||||
|     core.saveState(State.CacheResult, JSON.stringify(state)); | ||||
| } | ||||
| 
 | ||||
| export function setCacheHitOutput(isCacheHit: boolean): void { | ||||
|     core.setOutput(Outputs.CacheHit, isCacheHit.toString()); | ||||
| } | ||||
| 
 | ||||
| export function setOutputAndState( | ||||
|     key: string, | ||||
|     cacheResult?: ArtifactCacheEntry | ||||
| ) { | ||||
| ): void { | ||||
|     setCacheHitOutput(isExactKeyMatch(key, cacheResult)); | ||||
|     // Store the cache result if it exists
 | ||||
|     cacheResult && setCacheState(cacheResult); | ||||
| @ -57,15 +70,11 @@ export function setOutputAndState( | ||||
| export function getCacheState(): ArtifactCacheEntry | undefined { | ||||
|     const stateData = core.getState(State.CacheResult); | ||||
|     core.debug(`State: ${stateData}`); | ||||
|     return (stateData && JSON.parse(stateData)) as ArtifactCacheEntry; | ||||
|     if (stateData) { | ||||
|         return JSON.parse(stateData) as ArtifactCacheEntry; | ||||
|     } | ||||
| 
 | ||||
| export function setCacheState(state: ArtifactCacheEntry) { | ||||
|     core.saveState(State.CacheResult, JSON.stringify(state)); | ||||
| } | ||||
| 
 | ||||
| export function setCacheHitOutput(isCacheHit: boolean) { | ||||
|     core.setOutput(Outputs.CacheHit, isCacheHit.toString()); | ||||
|     return undefined; | ||||
| } | ||||
| 
 | ||||
| export function resolvePath(filePath: string): string { | ||||
| @ -79,3 +88,15 @@ export function resolvePath(filePath: string): string { | ||||
| 
 | ||||
|     return path.resolve(filePath); | ||||
| } | ||||
| 
 | ||||
| export function getSupportedEvents(): string[] { | ||||
|     return [Events.Push, Events.PullRequest]; | ||||
| } | ||||
| 
 | ||||
| // Currently the cache token is only authorized for push and pull_request events
 | ||||
| // All other events will fail when reading and saving the cache
 | ||||
| // 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 { | ||||
|     const githubEvent = process.env[Events.Key] || ""; | ||||
|     return getSupportedEvents().includes(githubEvent); | ||||
| } | ||||
|  | ||||
| @ -1,7 +1,29 @@ | ||||
| import { Inputs } from "../constants"; | ||||
| 
 | ||||
| // See: https://github.com/actions/toolkit/blob/master/packages/core/src/core.ts#L67
 | ||||
| function getInputName(name: string): string { | ||||
|     return `INPUT_${name.replace(/ /g, "_").toUpperCase()}`; | ||||
| } | ||||
| 
 | ||||
| export function setInput(name: string, value: string) { | ||||
| export function setInput(name: string, value: string): void { | ||||
|     process.env[getInputName(name)] = value; | ||||
| } | ||||
| 
 | ||||
| interface CacheInput { | ||||
|     path: string; | ||||
|     key: string; | ||||
|     restoreKeys?: string[]; | ||||
| } | ||||
| 
 | ||||
| export function setInputs(input: CacheInput): void { | ||||
|     setInput(Inputs.Path, input.path); | ||||
|     setInput(Inputs.Key, input.key); | ||||
|     input.restoreKeys && | ||||
|         setInput(Inputs.RestoreKeys, input.restoreKeys.join("\n")); | ||||
| } | ||||
| 
 | ||||
| export function clearInputs(): void { | ||||
|     delete process.env[getInputName(Inputs.Path)]; | ||||
|     delete process.env[getInputName(Inputs.Key)]; | ||||
|     delete process.env[getInputName(Inputs.RestoreKeys)]; | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Josh Gross
						Josh Gross