From 6491e51b664cab00918c9cf51a5b9705d94ff877 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Fri, 15 Nov 2019 10:29:58 -0500 Subject: [PATCH] Merge master into releases/v1 --- .eslintrc.json | 16 + .github/workflows/workflow.yml | 26 +- README.md | 25 +- __tests__/__fixtures__/helloWorld.txt | 1 + __tests__/actionUtils.test.ts | 226 ++++++ __tests__/main.test.ts | 22 - __tests__/restore.test.ts | 450 +++++++++++ __tests__/save.test.ts | 329 ++++++++ examples.md | 157 +++- jest.config.js | 39 +- package-lock.json | 1023 +++++++++++++++++++++++++ package.json | 10 + src/cacheHttpClient.ts | 90 +-- src/constants.ts | 24 +- src/restore.ts | 52 +- src/save.ts | 37 +- src/utils/actionUtils.ts | 41 +- src/utils/testUtils.ts | 24 +- 18 files changed, 2421 insertions(+), 171 deletions(-) create mode 100644 .eslintrc.json create mode 100644 __tests__/__fixtures__/helloWorld.txt create mode 100644 __tests__/actionUtils.test.ts delete mode 100644 __tests__/main.test.ts create mode 100644 __tests__/restore.test.ts create mode 100644 __tests__/save.test.ts diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..ccaf1a6 --- /dev/null +++ b/.eslintrc.json @@ -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"] +} diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 59bb21f..b31fdc1 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -1,6 +1,11 @@ name: Tests + on: pull_request: + branches: + - master + paths-ignore: + - '**.md' push: branches: - master @@ -10,22 +15,39 @@ on: jobs: test: name: Test on ${{ matrix.os }} + strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] + runs-on: ${{ matrix.os }} - + steps: - uses: actions/checkout@v1 - + - uses: actions/setup-node@v1 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 diff --git a/README.md b/README.md index e65cea6..210c51d 100644 --- a/README.md +++ b/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 @@ -78,7 +75,7 @@ steps: id: cache with: path: path/to/dependencies - key: ${{ runner.os }}-${{ hashFiles('**/lockfiles')}} + key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' diff --git a/__tests__/__fixtures__/helloWorld.txt b/__tests__/__fixtures__/helloWorld.txt new file mode 100644 index 0000000..95d09f2 --- /dev/null +++ b/__tests__/__fixtures__/helloWorld.txt @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/__tests__/actionUtils.test.ts b/__tests__/actionUtils.test.ts new file mode 100644 index 0000000..4688b5d --- /dev/null +++ b/__tests__/actionUtils.test.ts @@ -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); +}); diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts deleted file mode 100644 index 074a5e7..0000000 --- a/__tests__/main.test.ts +++ /dev/null @@ -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" - ); -}); diff --git a/__tests__/restore.test.ts b/__tests__/restore.test.ts new file mode 100644 index 0000000..1919e30 --- /dev/null +++ b/__tests__/restore.test.ts @@ -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); +}); diff --git a/__tests__/save.test.ts b/__tests__/save.test.ts new file mode 100644 index 0000000..67b13d2 --- /dev/null +++ b/__tests__/save.test.ts @@ -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); +}); diff --git a/examples.md b/examples.md index b844b41..7d66b62 100644 --- a/examples.md +++ b/examples.md @@ -1,16 +1,25 @@ # 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) -- [Node - Yarn](#node---yarn) -- [Ruby - Gem](#ruby---gem) -- [Rust - Cargo](#rust---cargo) -- [Swift, Objective-C - Carthage](#swift-objective-c---carthage) -- [Swift, Objective-C - CocoaPods](#swift-objective-c---cocoapods) +- [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) + - [Swift, Objective-C - CocoaPods](#swift-objective-c---cocoapods) ## C# - Nuget Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/package-references-in-project-files#locking-dependencies): @@ -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 diff --git a/jest.config.js b/jest.config.js index 42e7e56..59548f6 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,20 +1,23 @@ -module.exports = { - clearMocks: true, - moduleFileExtensions: ['js', 'ts'], - testEnvironment: 'node', - testMatch: ['**/*.test.ts'], - testRunner: 'jest-circus/runner', - transform: { - '^.+\\.ts$': 'ts-jest' - }, - verbose: true -} +require("nock").disableNetConnect(); -const processStdoutWrite = process.stdout.write.bind(process.stdout) +module.exports = { + clearMocks: true, + moduleFileExtensions: ["js", "ts"], + testEnvironment: "node", + testMatch: ["**/*.test.ts"], + testRunner: "jest-circus/runner", + transform: { + "^.+\\.ts$": "ts-jest" + }, + verbose: true +}; + +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); - } -} \ No newline at end of file + // 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); + } +}; diff --git a/package-lock.json b/package-lock.json index a3dc4ea..605ecc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -477,6 +477,12 @@ "@babel/types": "^7.3.0" } }, + "@types/eslint-visitor-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", + "integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==", + "dev": true + }, "@types/istanbul-lib-coverage": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", @@ -517,6 +523,21 @@ "integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==", "dev": true }, + "@types/json-schema": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz", + "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==", + "dev": true + }, + "@types/nock": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/@types/nock/-/nock-11.1.0.tgz", + "integrity": "sha512-jI/ewavBQ7X5178262JQR0ewicPAcJhXS/iFaNJl0VHLfyosZ/kwSrsa6VNQNSO8i9d8SqdRgOtZSOKJ/+iNMw==", + "dev": true, + "requires": { + "nock": "*" + } + }, "@types/node": { "version": "12.6.9", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.9.tgz", @@ -544,6 +565,73 @@ "integrity": "sha512-SOhuU4wNBxhhTHxYaiG5NY4HBhDIDnJF60GU+2LqHAdKKer86//e4yg69aENCtQ04n0ovz+tq2YPME5t5yp4pw==", "dev": true }, + "@typescript-eslint/eslint-plugin": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.7.0.tgz", + "integrity": "sha512-H5G7yi0b0FgmqaEUpzyBlVh0d9lq4cWG2ap0RKa6BkF3rpBb6IrAoubt1NWh9R2kRs/f0k6XwRDiDz3X/FqXhQ==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "2.7.0", + "eslint-utils": "^1.4.2", + "functional-red-black-tree": "^1.0.1", + "regexpp": "^2.0.1", + "tsutils": "^3.17.1" + } + }, + "@typescript-eslint/experimental-utils": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.7.0.tgz", + "integrity": "sha512-9/L/OJh2a5G2ltgBWJpHRfGnt61AgDeH6rsdg59BH0naQseSwR7abwHq3D5/op0KYD/zFT4LS5gGvWcMmegTEg==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/typescript-estree": "2.7.0", + "eslint-scope": "^5.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.7.0.tgz", + "integrity": "sha512-ctC0g0ZvYclxMh/xI+tyqP0EC2fAo6KicN9Wm2EIao+8OppLfxji7KAGJosQHSGBj3TcqUrA96AjgXuKa5ob2g==", + "dev": true, + "requires": { + "@types/eslint-visitor-keys": "^1.0.0", + "@typescript-eslint/experimental-utils": "2.7.0", + "@typescript-eslint/typescript-estree": "2.7.0", + "eslint-visitor-keys": "^1.1.0" + } + }, + "@typescript-eslint/typescript-estree": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.7.0.tgz", + "integrity": "sha512-vVCE/DY72N4RiJ/2f10PTyYekX2OLaltuSIBqeHYI44GQ940VCYioInIb8jKMrK9u855OEJdFC+HmWAZTnC+Ag==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "glob": "^7.1.4", + "is-glob": "^4.0.1", + "lodash.unescape": "4.0.1", + "semver": "^6.3.0", + "tsutils": "^3.17.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "@zeit/ncc": { "version": "0.20.5", "resolved": "https://registry.npmjs.org/@zeit/ncc/-/ncc-0.20.5.tgz", @@ -580,6 +668,12 @@ } } }, + "acorn-jsx": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz", + "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw==", + "dev": true + }, "acorn-walk": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", @@ -629,6 +723,15 @@ "normalize-path": "^2.1.1" } }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -653,6 +756,16 @@ "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", "dev": true }, + "array-includes": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.0.3.tgz", + "integrity": "sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0=", + "dev": true, + "requires": { + "define-properties": "^1.1.2", + "es-abstract": "^1.7.0" + } + }, "array-unique": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", @@ -674,6 +787,12 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", "dev": true }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true + }, "assign-symbols": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", @@ -963,6 +1082,12 @@ } } }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", @@ -984,6 +1109,20 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, + "chai": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.2.0.tgz", + "integrity": "sha512-XQU3bhBukrOsQCuwZndwGcCVQHyZi53fQ6Ys1Fym7E4olpIqqZZhhoFJoaKVvV17lWQoXYwgWN2nF5crA8J2jw==", + "dev": true, + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.0", + "type-detect": "^4.0.5" + } + }, "chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -995,6 +1134,18 @@ "supports-color": "^5.3.0" } }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "dev": true + }, "ci-info": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", @@ -1030,6 +1181,21 @@ } } }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-width": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz", + "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk=", + "dev": true + }, "cliui": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", @@ -1123,6 +1289,12 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", "dev": true }, + "contains-path": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", + "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", + "dev": true + }, "convert-source-map": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", @@ -1234,6 +1406,15 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dev": true, + "requires": { + "type-detect": "^4.0.0" + } + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", @@ -1314,6 +1495,15 @@ "integrity": "sha512-xLqpez+Zj9GKSnPWS0WZw1igGocZ+uua8+y+5dDNTT934N3QuY1sp2LkHzwiaYQGz60hMq0pjAshdeXm5VUOEw==", "dev": true }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, "domexception": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", @@ -1333,6 +1523,12 @@ "safer-buffer": "^2.1.0" } }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "end-of-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", @@ -1403,6 +1599,267 @@ } } }, + "eslint": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.6.0.tgz", + "integrity": "sha512-PpEBq7b6qY/qrOmpYQ/jTMDYfuQMELR4g4WI1M/NaSDDD/bdcMb+dj4Hgks7p41kW2caXsPsEZAEAyAgjVVC0g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "ajv": "^6.10.0", + "chalk": "^2.1.0", + "cross-spawn": "^6.0.5", + "debug": "^4.0.1", + "doctrine": "^3.0.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^1.4.3", + "eslint-visitor-keys": "^1.1.0", + "espree": "^6.1.2", + "esquery": "^1.0.1", + "esutils": "^2.0.2", + "file-entry-cache": "^5.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^5.0.0", + "globals": "^11.7.0", + "ignore": "^4.0.6", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "inquirer": "^7.0.0", + "is-glob": "^4.0.0", + "js-yaml": "^3.13.1", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.3.0", + "lodash": "^4.17.14", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "natural-compare": "^1.4.0", + "optionator": "^0.8.2", + "progress": "^2.0.0", + "regexpp": "^2.0.1", + "semver": "^6.1.2", + "strip-ansi": "^5.2.0", + "strip-json-comments": "^3.0.1", + "table": "^5.2.3", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.5.0.tgz", + "integrity": "sha512-cjXp8SbO9VFGW/Z7mbTydqS9to8Z58E5aYhj3e1+Hx7lS9s6gL5ILKNpCqZAFOVYRcSkWPFYljHrEh8QFEK5EQ==", + "dev": true, + "requires": { + "get-stdin": "^6.0.0" + } + }, + "eslint-import-resolver-node": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz", + "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==", + "dev": true, + "requires": { + "debug": "^2.6.9", + "resolve": "^1.5.0" + } + }, + "eslint-module-utils": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz", + "integrity": "sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw==", + "dev": true, + "requires": { + "debug": "^2.6.8", + "pkg-dir": "^2.0.0" + } + }, + "eslint-plugin-import": { + "version": "2.18.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz", + "integrity": "sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ==", + "dev": true, + "requires": { + "array-includes": "^3.0.3", + "contains-path": "^0.1.0", + "debug": "^2.6.9", + "doctrine": "1.5.0", + "eslint-import-resolver-node": "^0.3.2", + "eslint-module-utils": "^2.4.0", + "has": "^1.0.3", + "minimatch": "^3.0.4", + "object.values": "^1.1.0", + "read-pkg-up": "^2.0.0", + "resolve": "^1.11.0" + }, + "dependencies": { + "doctrine": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", + "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", + "dev": true, + "requires": { + "esutils": "^2.0.2", + "isarray": "^1.0.0" + } + }, + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + }, + "read-pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", + "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", + "dev": true, + "requires": { + "find-up": "^2.0.0", + "read-pkg": "^2.0.0" + } + } + } + }, + "eslint-plugin-jest": { + "version": "23.0.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-23.0.3.tgz", + "integrity": "sha512-9cNxr66zeOyz1S9AkQL4/ouilR6QHpYj8vKOQZ60fu9hAt5PJWS4KqWqfr1aqN5NFEZSPjFOla2Azn+KTWiGwg==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "^2.5.0" + } + }, + "eslint-plugin-prettier": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.1.tgz", + "integrity": "sha512-A+TZuHZ0KU0cnn56/9mfR7/KjUJ9QNVXUhwvRFSR7PGPe0zQR6PTkmyqg1AtUUEOzTqeRsUwyKFh0oVZKVCrtA==", + "dev": true, + "requires": { + "prettier-linter-helpers": "^1.0.0" + } + }, + "eslint-scope": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz", + "integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==", + "dev": true, + "requires": { + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", + "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^1.1.0" + } + }, + "eslint-visitor-keys": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz", + "integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==", + "dev": true + }, + "espree": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-6.1.2.tgz", + "integrity": "sha512-2iUPuuPP+yW1PZaMSDM9eyVf8D5P0Hi8h83YtZ5bPc/zHYjII5khoixIUTMO794NOY8F/ThF1Bo8ncZILarUTA==", + "dev": true, + "requires": { + "acorn": "^7.1.0", + "acorn-jsx": "^5.1.0", + "eslint-visitor-keys": "^1.1.0" + }, + "dependencies": { + "acorn": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", + "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", + "dev": true + } + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "esquery": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", + "dev": true, + "requires": { + "estraverse": "^4.0.0" + } + }, + "esrecurse": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", + "dev": true, + "requires": { + "estraverse": "^4.1.0" + } + }, "estraverse": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", @@ -1533,6 +1990,17 @@ } } }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -1610,6 +2078,12 @@ "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", "dev": true }, + "fast-diff": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", + "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", + "dev": true + }, "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", @@ -1631,6 +2105,24 @@ "bser": "^2.0.0" } }, + "figures": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.1.0.tgz", + "integrity": "sha512-ravh8VRXqHuMvZt/d8GblBeqDMkdJMBdv/2KntFH+ra5MXkO7nxNKpzQ3n6QD/2da1kH0aWmNISdvhM7gl2gVg==", + "dev": true, + "requires": { + "escape-string-regexp": "^1.0.5" + } + }, + "file-entry-cache": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", + "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", + "dev": true, + "requires": { + "flat-cache": "^2.0.1" + } + }, "fill-range": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", @@ -1663,6 +2155,23 @@ "locate-path": "^3.0.0" } }, + "flat-cache": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", + "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", + "dev": true, + "requires": { + "flatted": "^2.0.0", + "rimraf": "2.6.3", + "write": "1.0.3" + } + }, + "flatted": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.1.tgz", + "integrity": "sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg==", + "dev": true + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", @@ -2255,12 +2764,30 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "dev": true + }, "get-caller-file": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", "dev": true }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "dev": true + }, + "get-stdin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", + "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", + "dev": true + }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -2299,6 +2826,15 @@ "path-is-absolute": "^1.0.0" } }, + "glob-parent": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.0.tgz", + "integrity": "sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, "globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -2441,6 +2977,30 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ignore": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", + "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", + "dev": true + }, + "import-fresh": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.1.0.tgz", + "integrity": "sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, "import-local": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", @@ -2484,6 +3044,72 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "inquirer": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.0.0.tgz", + "integrity": "sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "chalk": "^2.4.2", + "cli-cursor": "^3.1.0", + "cli-width": "^2.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.15", + "mute-stream": "0.0.8", + "run-async": "^2.2.0", + "rxjs": "^6.4.0", + "string-width": "^4.1.0", + "strip-ansi": "^5.1.0", + "through": "^2.3.6" + }, + "dependencies": { + "ansi-escapes": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.2.1.tgz", + "integrity": "sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q==", + "dev": true, + "requires": { + "type-fest": "^0.5.2" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + } + } + }, "invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -2597,6 +3223,12 @@ "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", "dev": true }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true + }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", @@ -2609,6 +3241,15 @@ "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", "dev": true }, + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -2629,6 +3270,12 @@ } } }, + "is-promise": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz", + "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o=", + "dev": true + }, "is-regex": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", @@ -3255,6 +3902,16 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -3319,6 +3976,12 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true + }, "json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -3423,6 +4086,12 @@ "integrity": "sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=", "dev": true }, + "lodash.unescape": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz", + "integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=", + "dev": true + }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -3631,6 +4300,12 @@ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", "dev": true }, + "mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, "nan": { "version": "2.14.0", "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz", @@ -3675,6 +4350,37 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nock": { + "version": "11.7.0", + "resolved": "https://registry.npmjs.org/nock/-/nock-11.7.0.tgz", + "integrity": "sha512-7c1jhHew74C33OBeRYyQENT+YXQiejpwIrEjinh6dRurBae+Ei4QjeUaPlkptIF0ZacEiVCnw8dWaxqepkiihg==", + "dev": true, + "requires": { + "chai": "^4.1.2", + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.13", + "mkdirp": "^0.5.0", + "propagate": "^2.0.0" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -3845,6 +4551,18 @@ } } }, + "object.values": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.0.tgz", + "integrity": "sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg==", + "dev": true, + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.12.0", + "function-bind": "^1.1.1", + "has": "^1.0.3" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -3854,6 +4572,15 @@ "wrappy": "1" } }, + "onetime": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", + "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, "optimist": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", @@ -3905,6 +4632,12 @@ "mem": "^4.0.0" } }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true + }, "p-defer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", @@ -3962,6 +4695,15 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, "parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", @@ -4017,6 +4759,12 @@ "pify": "^3.0.0" } }, + "pathval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", + "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=", + "dev": true + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -4038,6 +4786,60 @@ "node-modules-regexp": "^1.0.0" } }, + "pkg-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", + "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + } + } + }, "pn": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", @@ -4062,6 +4864,15 @@ "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", "dev": true }, + "prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "requires": { + "fast-diff": "^1.1.2" + } + }, "pretty-format": { "version": "24.8.0", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-24.8.0.tgz", @@ -4080,6 +4891,12 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true + }, "prompts": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.1.0.tgz", @@ -4090,6 +4907,12 @@ "sisteransi": "^1.0.0" } }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, "psl": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.3.0.tgz", @@ -4124,6 +4947,55 @@ "integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==", "dev": true }, + "read-pkg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", + "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", + "dev": true, + "requires": { + "load-json-file": "^2.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^2.0.0" + }, + "dependencies": { + "load-json-file": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", + "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "strip-bom": "^3.0.0" + } + }, + "parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", + "dev": true, + "requires": { + "error-ex": "^1.2.0" + } + }, + "path-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", + "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", + "dev": true, + "requires": { + "pify": "^2.0.0" + } + }, + "pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", + "dev": true + } + } + }, "read-pkg-up": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-4.0.0.tgz", @@ -4181,6 +5053,12 @@ "safe-regex": "^1.1.0" } }, + "regexpp": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", + "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", + "dev": true + }, "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", @@ -4307,6 +5185,16 @@ "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", "dev": true }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + } + }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -4328,6 +5216,24 @@ "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", "dev": true }, + "run-async": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz", + "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=", + "dev": true, + "requires": { + "is-promise": "^2.1.0" + } + }, + "rxjs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", + "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==", + "dev": true, + "requires": { + "tslib": "^1.9.0" + } + }, "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -4455,6 +5361,17 @@ "integrity": "sha512-ZcYcZcT69nSLAR2oLN2JwNmLkJEKGooFMCdvOkFrToUt/WfcRWqhIg4P4KwY4dmLbuyXIx4o4YmPsvMRJYJd/w==", "dev": true }, + "slice-ansi": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", + "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.0", + "astral-regex": "^1.0.0", + "is-fullwidth-code-point": "^2.0.0" + } + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -4650,6 +5567,12 @@ "extend-shallow": "^3.0.0" } }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -4784,6 +5707,12 @@ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true }, + "strip-json-comments": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.0.1.tgz", + "integrity": "sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw==", + "dev": true + }, "supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -4799,6 +5728,37 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "table": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", + "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", + "dev": true, + "requires": { + "ajv": "^6.10.2", + "lodash": "^4.17.14", + "slice-ansi": "^2.1.0", + "string-width": "^3.0.0" + }, + "dependencies": { + "emoji-regex": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", + "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", + "dev": true + }, + "string-width": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", + "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", + "dev": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + } + } + }, "test-exclude": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-5.2.3.tgz", @@ -4811,12 +5771,33 @@ "require-main-filename": "^2.0.0" } }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true + }, "throat": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", "dev": true }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", + "dev": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, "tmpl": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", @@ -4936,6 +5917,21 @@ } } }, + "tslib": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", + "dev": true + }, + "tsutils": { + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz", + "integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "tunnel": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.4.tgz", @@ -4965,6 +5961,18 @@ "prelude-ls": "~1.1.2" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz", + "integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==", + "dev": true + }, "typed-rest-client": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.5.0.tgz", @@ -5096,6 +6104,12 @@ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==" }, + "v8-compile-cache": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz", + "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", + "dev": true + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -5241,6 +6255,15 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "write": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", + "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", + "dev": true, + "requires": { + "mkdirp": "^0.5.1" + } + }, "write-file-atomic": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.4.1.tgz", diff --git a/package.json b/package.json index a235bd7..5bece1c 100644 --- a/package.json +++ b/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" diff --git a/src/cacheHttpClient.ts b/src/cacheHttpClient.ts index 30b5009..e448157 100644 --- a/src/cacheHttpClient.ts +++ b/src/cacheHttpClient.ts @@ -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 { @@ -43,16 +70,6 @@ export async function getCacheEntry( return cacheResult; } -export async function downloadCache( - cacheEntry: ArtifactCacheEntry, - archivePath: string -): Promise { - 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 { + 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 { + 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; -} diff --git a/src/constants.ts b/src/constants.ts index 80f6de9..5f26e8c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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" } diff --git a/src/restore.ts b/src/restore.ts index 060c8d4..87a2684 100644 --- a/src/restore.ts +++ b/src/restore.ts @@ -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 { 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] [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( diff --git a/src/save.ts b/src/save.ts index 69e44cf..d660064 100644 --- a/src/save.ts +++ b/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 { 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] [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); } diff --git a/src/utils/actionUtils.ts b/src/utils/actionUtils.ts index d4d7638..ba5b2ea 100644 --- a/src/utils/actionUtils.ts +++ b/src/utils/actionUtils.ts @@ -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 { 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); +} diff --git a/src/utils/testUtils.ts b/src/utils/testUtils.ts index 67121c7..b1d20d0 100644 --- a/src/utils/testUtils.ts +++ b/src/utils/testUtils.ts @@ -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)]; +}