From f66a56e59e05865f438a80db86275959efb92901 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Mon, 4 Nov 2019 16:40:33 -0500 Subject: [PATCH 01/37] Bump version to v1 (#51) --- README.md | 4 ++-- examples.md | 26 +++++++++++++------------- package-lock.json | 2 +- package.json | 2 +- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index a289d65..ccf4bc5 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ jobs: - uses: actions/checkout@v1 - name: Cache node modules - uses: actions/cache@preview + uses: actions/cache@v1 with: path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -70,7 +70,7 @@ Example: steps: - uses: actions/checkout@v1 - - uses: actions/cache@preview + - uses: actions/cache@v1 id: cache with: path: path/to/dependencies diff --git a/examples.md b/examples.md index 54c05bb..ca571d8 100644 --- a/examples.md +++ b/examples.md @@ -15,7 +15,7 @@ ## Node - npm ```yaml -- uses: actions/cache@preview +- uses: actions/cache@v1 with: path: node_modules key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} @@ -26,7 +26,7 @@ ## Node - Yarn ```yaml -- uses: actions/cache@preview +- uses: actions/cache@v1 with: path: ~/.cache/yarn key: ${{ runner.os }}-yarn-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }} @@ -37,7 +37,7 @@ ## C# - Nuget Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/package-references-in-project-files#locking-dependencies): ```yaml -- uses: actions/cache@preview +- uses: actions/cache@v1 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} @@ -48,7 +48,7 @@ Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/packa ## Java - Gradle ```yaml -- uses: actions/cache@preview +- uses: actions/cache@v1 with: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} @@ -59,7 +59,7 @@ Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/packa ## Java - Maven ```yaml -- uses: actions/cache@preview +- uses: actions/cache@v1 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} @@ -70,7 +70,7 @@ Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/packa ## Swift, Objective-C - Carthage ```yaml -uses: actions/cache@preview +uses: actions/cache@v1 with: path: Carthage key: ${{ runner.os }}-carthage-${{ hashFiles('**/Cartfile.resolved') }} @@ -81,7 +81,7 @@ uses: actions/cache@preview ## Swift, Objective-C - CocoaPods ```yaml -- uses: actions/cache@preview +- uses: actions/cache@v1 with: path: Pods key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} @@ -92,7 +92,7 @@ uses: actions/cache@preview ## Ruby - Gem ```yaml -- uses: actions/cache@preview +- uses: actions/cache@v1 with: path: vendor/bundle key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} @@ -103,7 +103,7 @@ uses: actions/cache@preview ## Go - Modules ```yaml -- uses: actions/cache@preview +- uses: actions/cache@v1 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} @@ -113,7 +113,7 @@ uses: actions/cache@preview ## Elixir - Mix ```yaml -- uses: actions/cache@preview +- uses: actions/cache@v1 with: path: deps key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} @@ -125,17 +125,17 @@ uses: actions/cache@preview ``` - name: Cache cargo registry - uses: actions/cache@preview + uses: actions/cache@v1 with: path: ~/.cargo/registry key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - name: Cache cargo index - uses: actions/cache@preview + uses: actions/cache@v1 with: path: ~/.cargo/git key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} - name: Cache cargo build - uses: actions/cache@preview + uses: actions/cache@v1 with: path: target key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} diff --git a/package-lock.json b/package-lock.json index ed53148..59a5bb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cache", - "version": "0.0.1", + "version": "1.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f5ce058..84b7733 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cache", - "version": "0.0.2", + "version": "1.0.0", "private": true, "description": "Cache dependencies and build outputs", "main": "dist/restore/index.js", From d8c5e69fe251a72a4f0d124e874a4d06a04cd975 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Tue, 5 Nov 2019 10:59:26 -0500 Subject: [PATCH 02/37] Update badge to filter to master push events --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ccf4bc5..3bce57f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This GitHub Action allows caching dependencies and build outputs to improve workflow execution time. -GitHub Actions status +GitHub Actions status ## Usage From 5d3ad75a2b94d82da5b2428f0e78ad55f255930f Mon Sep 17 00:00:00 2001 From: Koen Punt Date: Tue, 5 Nov 2019 17:03:56 +0100 Subject: [PATCH 03/37] Update example formatting (#57) * adjust formatting of Carthage example * enable syntax highlighting for Cargo example --- examples.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/examples.md b/examples.md index ca571d8..8ba2bd7 100644 --- a/examples.md +++ b/examples.md @@ -36,6 +36,7 @@ ## C# - Nuget Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/package-references-in-project-files#locking-dependencies): + ```yaml - uses: actions/cache@v1 with: @@ -70,12 +71,12 @@ Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/packa ## Swift, Objective-C - Carthage ```yaml -uses: actions/cache@v1 - with: - path: Carthage - key: ${{ runner.os }}-carthage-${{ hashFiles('**/Cartfile.resolved') }} - restore-keys: | - ${{ runner.os }}-carthage- +- uses: actions/cache@v1 + with: + path: Carthage + key: ${{ runner.os }}-carthage-${{ hashFiles('**/Cartfile.resolved') }} + restore-keys: | + ${{ runner.os }}-carthage- ``` ## Swift, Objective-C - CocoaPods @@ -123,7 +124,7 @@ uses: actions/cache@v1 ## Rust - Cargo -``` +```yaml - name: Cache cargo registry uses: actions/cache@v1 with: From 5f4d4d4555bb4eb436bfe180df7759b23a031f8a Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 5 Nov 2019 19:04:07 +0200 Subject: [PATCH 04/37] Alphabetise examples (#52) --- examples.md | 114 ++++++++++++++++++++++++++-------------------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/examples.md b/examples.md index 8ba2bd7..b844b41 100644 --- a/examples.md +++ b/examples.md @@ -1,38 +1,16 @@ # Examples -- [Node - npm](#node---npm) -- [Node - Yarn](#node---yarn) - [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) -- [Ruby - Gem](#ruby---gem) -- [Go - Modules](#go---modules) -- [Elixir - Mix](#elixir---mix) -- [Rust - Cargo](#rust---cargo) - -## Node - npm - -```yaml -- uses: actions/cache@v1 - with: - path: node_modules - key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} - restore-keys: | - ${{ runner.os }}-node- -``` - -## Node - Yarn - -```yaml -- uses: actions/cache@v1 - with: - path: ~/.cache/yarn - key: ${{ runner.os }}-yarn-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }} - restore-keys: | - ${{ runner.os }}-yarn- -``` ## C# - Nuget Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/package-references-in-project-files#locking-dependencies): @@ -46,6 +24,27 @@ Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/packa ${{ runner.os }}-nuget- ``` +## Elixir - Mix +```yaml +- uses: actions/cache@v1 + with: + path: deps + key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} + restore-keys: | + ${{ runner.os }}-mix- +``` + +## Go - Modules + +```yaml +- uses: actions/cache@v1 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- +``` + ## Java - Gradle ```yaml @@ -68,26 +67,26 @@ Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/packa ${{ runner.os }}-maven- ``` -## Swift, Objective-C - Carthage +## Node - npm ```yaml - uses: actions/cache@v1 with: - path: Carthage - key: ${{ runner.os }}-carthage-${{ hashFiles('**/Cartfile.resolved') }} + path: node_modules + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | - ${{ runner.os }}-carthage- + ${{ runner.os }}-node- ``` -## Swift, Objective-C - CocoaPods +## Node - Yarn ```yaml - uses: actions/cache@v1 with: - path: Pods - key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} + path: ~/.cache/yarn + key: ${{ runner.os }}-yarn-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }} restore-keys: | - ${{ runner.os }}-pods- + ${{ runner.os }}-yarn- ``` ## Ruby - Gem @@ -101,27 +100,6 @@ Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/packa ${{ runner.os }}-gem- ``` -## Go - Modules - -```yaml -- uses: actions/cache@v1 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- -``` - -## Elixir - Mix -```yaml -- uses: actions/cache@v1 - with: - path: deps - key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }} - restore-keys: | - ${{ runner.os }}-mix- -``` - ## Rust - Cargo ```yaml @@ -141,3 +119,25 @@ Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/packa path: target key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} ``` + +## Swift, Objective-C - Carthage + +```yaml +- uses: actions/cache@v1 + with: + path: Carthage + key: ${{ runner.os }}-carthage-${{ hashFiles('**/Cartfile.resolved') }} + restore-keys: | + ${{ runner.os }}-carthage- +``` + +## Swift, Objective-C - CocoaPods + +```yaml +- uses: actions/cache@v1 + with: + path: Pods + key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} + restore-keys: | + ${{ runner.os }}-pods- +``` From e1ed41a9c913f73dd08e261583c25e9d5eca5007 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Tue, 5 Nov 2019 21:09:13 +0200 Subject: [PATCH 05/37] Link to docs (#58) * Link to docs * Attempt to default to user's browser language first --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 3bce57f..9b5205b 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ This GitHub Action allows caching dependencies and build outputs to improve work GitHub Actions status +## Documentation + +See ["Caching dependencies to speed up workflows"](https://help.github.com/github/automating-your-workflow-with-github-actions/caching-dependencies-to-speed-up-workflows). + ## Usage ### Pre-requisites From b034b26a443e7d8910e7ee72b6029579cabc4a3c Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Tue, 5 Nov 2019 15:24:22 -0500 Subject: [PATCH 06/37] Bump cache limit to 400MB (#61) --- README.md | 2 +- src/save.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9b5205b..e65cea6 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ See [Examples](examples.md) ## Cache Limits -Individual caches are limited to 200MB and a repository can have up to 2GB of caches. Once the 2GB limit is reached, older caches will be evicted based on when the cache was last accessed. Caches that are not accessed within the last week will also be evicted. +Individual caches are limited to 400MB and a repository can have up to 2GB of caches. Once the 2GB limit is reached, older caches will be evicted based on when the cache was last accessed. Caches that are not accessed within the last week will also be evicted. ## Skipping steps based on cache-hit diff --git a/src/save.ts b/src/save.ts index f688e70..69e44cf 100644 --- a/src/save.ts +++ b/src/save.ts @@ -54,12 +54,12 @@ async function run() { core.debug(`Tar Path: ${tarPath}`); await exec(`"${tarPath}"`, args); - const fileSizeLimit = 200 * 1024 * 1024; // 200MB + const fileSizeLimit = 400 * 1024 * 1024; // 400MB const archiveFileSize = fs.statSync(archivePath).size; core.debug(`File Size: ${archiveFileSize}`); if (archiveFileSize > fileSizeLimit) { core.warning( - `Cache size of ${archiveFileSize} bytes is over the 200MB limit, not saving cache.` + `Cache size of ${archiveFileSize} bytes is over the 400MB limit, not saving cache.` ); return; } From 30524a6fbd4acbd14e57b650e30000157452172b Mon Sep 17 00:00:00 2001 From: Birunthan Mohanathas Date: Tue, 5 Nov 2019 14:33:41 -0600 Subject: [PATCH 07/37] Tweak 'Cache not found' message (#54) Previously the message was like this: ``` Cache not found for input keys: ["xxx",""] ``` Note the empty entry at the end because `String.prototype.split` results in an array with one empty string if there was nothing to split. Now it looks like: ``` Cache not found for input keys: xxx ``` --- src/restore.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/restore.ts b/src/restore.ts index b115b08..060c8d4 100644 --- a/src/restore.ts +++ b/src/restore.ts @@ -20,7 +20,10 @@ async function run() { const primaryKey = core.getInput(Inputs.Key, { required: true }); core.saveState(State.CacheKey, primaryKey); - const restoreKeys = core.getInput(Inputs.RestoreKeys).split("\n"); + const restoreKeys = core + .getInput(Inputs.RestoreKeys) + .split("\n") + .filter(x => x !== ""); const keys = [primaryKey, ...restoreKeys]; core.debug("Resolved Keys:"); @@ -52,7 +55,7 @@ async function run() { const cacheEntry = await cacheHttpClient.getCacheEntry(keys); if (!cacheEntry) { core.info( - `Cache not found for input keys: ${JSON.stringify(keys)}.` + `Cache not found for input keys: ${keys.join(", ")}.` ); return; } From eb10706a9d8907823385661400a3c2db9e1080ce Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Tue, 5 Nov 2019 15:40:20 -0500 Subject: [PATCH 08/37] Bump version to v1.0.1 and audit fix --- package-lock.json | 22 +++++++++++----------- package.json | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index 59a5bb6..a3dc4ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cache", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1105,9 +1105,9 @@ } }, "commander": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.1.tgz", - "integrity": "sha512-cCuLsMhJeWQ/ZpsFTbE765kvVfoeSddc4nU3up4fV+fDBcfUXnbITJ+JzhkdjzOqhURjZgujxaioam4RM9yGUg==", + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true, "optional": true }, @@ -2318,9 +2318,9 @@ "dev": true }, "handlebars": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.2.tgz", - "integrity": "sha512-cIv17+GhL8pHHnRJzGu2wwcthL5sb8uDKBHvZ2Dtu5s1YNt0ljbzKbamnc+gr69y7bzwQiBdr5+hOpRd5pnOdg==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.1.tgz", + "integrity": "sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -4981,13 +4981,13 @@ "dev": true }, "uglify-js": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz", - "integrity": "sha512-W+jrUHJr3DXKhrsS7NUVxn3zqMOFn0hL/Ei6v0anCIMoKC93TjcflTagwIHLW7SfMFfiQuktQyFVCFHGUE0+yg==", + "version": "3.6.7", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.7.tgz", + "integrity": "sha512-4sXQDzmdnoXiO+xvmTzQsfIiwrjUCSA95rSP4SEd8tDb51W2TiDOlL76Hl+Kw0Ie42PSItCW8/t6pBNCF2R48A==", "dev": true, "optional": true, "requires": { - "commander": "~2.20.0", + "commander": "~2.20.3", "source-map": "~0.6.1" } }, diff --git a/package.json b/package.json index 84b7733..a235bd7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cache", - "version": "1.0.0", + "version": "1.0.1", "private": true, "description": "Cache dependencies and build outputs", "main": "dist/restore/index.js", From ecf6eea7083ac3a7f0d0ea39fe7c605d3c24e982 Mon Sep 17 00:00:00 2001 From: Kai Neuwerth Date: Tue, 5 Nov 2019 22:18:49 +0100 Subject: [PATCH 09/37] Add PHP Composer example (#32) --- examples.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/examples.md b/examples.md index b844b41..5a3c93f 100644 --- a/examples.md +++ b/examples.md @@ -7,6 +7,7 @@ - [Java - Maven](#java---maven) - [Node - npm](#node---npm) - [Node - Yarn](#node---yarn) +- [PHP - Composer](#php---composer) - [Ruby - Gem](#ruby---gem) - [Rust - Cargo](#rust---cargo) - [Swift, Objective-C - Carthage](#swift-objective-c---carthage) @@ -89,6 +90,21 @@ Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/packa ${{ 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- +``` + ## Ruby - Gem ```yaml From 4b0709a0d5bb7cd20f2ca9b9eb3dd620875faec9 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Wed, 6 Nov 2019 13:41:45 -0500 Subject: [PATCH 10/37] Add unit tests for restore (#62) * Move archive file size to utils * Disable net connect with nock * Add unit tests for restore * Fix test names and test URL --- __tests__/main.test.ts | 22 --- __tests__/restore.test.ts | 336 ++++++++++++++++++++++++++++++++++++++ jest.config.js | 2 + package-lock.json | 99 +++++++++++ package.json | 2 + src/restore.ts | 7 +- src/utils/actionUtils.ts | 5 + src/utils/testUtils.ts | 22 +++ 8 files changed, 469 insertions(+), 26 deletions(-) delete mode 100644 __tests__/main.test.ts create mode 100644 __tests__/restore.test.ts 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..e854f6c --- /dev/null +++ b/__tests__/restore.test.ts @@ -0,0 +1,336 @@ +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/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(io, "which").mockImplementation(tool => { + return Promise.resolve(tool); + }); +}); +afterEach(() => { + testUtils.clearInputs(); +}); + +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 tarArchivePath = IS_WINDOWS + ? archivePath.replace(/\\/g, "/") + : archivePath; + const tarCachePath = IS_WINDOWS ? cachePath.replace(/\\/g, "/") : cachePath; + const args = IS_WINDOWS ? ["-xz", "--force-local"] : ["-xz"]; + args.push(...["-f", tarArchivePath, "-C", tarCachePath]); + + 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(mkdirMock).toHaveBeenCalledWith(cachePath); + + const IS_WINDOWS = process.platform === "win32"; + const tarArchivePath = IS_WINDOWS + ? archivePath.replace(/\\/g, "/") + : archivePath; + const tarCachePath = IS_WINDOWS ? cachePath.replace(/\\/g, "/") : cachePath; + const args = IS_WINDOWS ? ["-xz", "--force-local"] : ["-xz"]; + args.push(...["-f", tarArchivePath, "-C", tarCachePath]); + + 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/jest.config.js b/jest.config.js index 42e7e56..095e90a 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,3 +1,5 @@ +require('nock').disableNetConnect(); + module.exports = { clearMocks: true, moduleFileExtensions: ['js', 'ts'], diff --git a/package-lock.json b/package-lock.json index a3dc4ea..26a0903 100644 --- a/package-lock.json +++ b/package-lock.json @@ -517,6 +517,15 @@ "integrity": "sha512-yALhelO3i0hqZwhjtcr6dYyaLoCHbAMshwtj6cGxTvHZAKXHsYGdff6E8EPw3xLKY0ELUTQ69Q1rQiJENnccMA==", "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", @@ -674,6 +683,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", @@ -984,6 +999,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 +1024,12 @@ "supports-color": "^5.3.0" } }, + "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", @@ -1234,6 +1269,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", @@ -2261,6 +2305,12 @@ "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-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -3675,6 +3725,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", @@ -4017,6 +4098,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", @@ -4090,6 +4177,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", @@ -4965,6 +5058,12 @@ "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 + }, "typed-rest-client": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.5.0.tgz", diff --git a/package.json b/package.json index a235bd7..568e958 100644 --- a/package.json +++ b/package.json @@ -31,11 +31,13 @@ }, "devDependencies": { "@types/jest": "^24.0.13", + "@types/nock": "^11.1.0", "@types/node": "^12.0.4", "@types/uuid": "^3.4.5", "@zeit/ncc": "^0.20.5", "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/restore.ts b/src/restore.ts index 060c8d4..4080098 100644 --- a/src/restore.ts +++ b/src/restore.ts @@ -2,7 +2,6 @@ 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"; @@ -72,6 +71,9 @@ async function run() { // Download the cache from the cache entry await cacheHttpClient.downloadCache(cacheEntry, archivePath); + const archiveFileSize = utils.getArchiveFileSize(archivePath); + core.debug(`File Size: ${archiveFileSize}`); + io.mkdirP(cachePath); // http://man7.org/linux/man-pages/man1/tar.1.html @@ -89,9 +91,6 @@ async function run() { 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/utils/actionUtils.ts b/src/utils/actionUtils.ts index d4d7638..9ad0072 100644 --- a/src/utils/actionUtils.ts +++ b/src/utils/actionUtils.ts @@ -1,5 +1,6 @@ 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"; @@ -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 diff --git a/src/utils/testUtils.ts b/src/utils/testUtils.ts index 67121c7..87cfc4f 100644 --- a/src/utils/testUtils.ts +++ b/src/utils/testUtils.ts @@ -1,3 +1,6 @@ +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()}`; } @@ -5,3 +8,22 @@ function getInputName(name: string): string { export function setInput(name: string, value: string) { process.env[getInputName(name)] = value; } + +interface CacheInput { + path: string; + key: string; + restoreKeys?: string[]; +} + +export function setInputs(input: CacheInput) { + setInput(Inputs.Path, input.path); + setInput(Inputs.Key, input.key); + input.restoreKeys && + setInput(Inputs.RestoreKeys, input.restoreKeys.join("\n")); +} + +export function clearInputs() { + delete process.env[getInputName(Inputs.Path)]; + delete process.env[getInputName(Inputs.Key)]; + delete process.env[getInputName(Inputs.RestoreKeys)]; +} From bde557aefda9083fd350b35d09c4a872b093644f Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Thu, 7 Nov 2019 20:02:06 -0500 Subject: [PATCH 11/37] Fix PR filters --- .github/workflows/workflow.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 59bb21f..a8f4407 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -1,6 +1,10 @@ name: Tests on: pull_request: + branches: + - master + paths-ignore: + - '**.md' push: branches: - master From bc821d0c12b430b965fdc132e1a32a346ce876d4 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Thu, 7 Nov 2019 21:04:46 -0500 Subject: [PATCH 12/37] Remove recommendation to cache node_modules (#69) * Update npm caching examples * Fix output name * Remove extra details tag --- README.md | 23 ++++++++++------------- examples.md | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index e65cea6..e8d048b 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 diff --git a/examples.md b/examples.md index 5a3c93f..2d5a8a7 100644 --- a/examples.md +++ b/examples.md @@ -70,10 +70,42 @@ 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- From 31508256ffcc3a8885c2d7a7737d1fe975ec414b Mon Sep 17 00:00:00 2001 From: BSKY Date: Wed, 13 Nov 2019 05:33:22 +0900 Subject: [PATCH 13/37] Update README.md (#76) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e8d048b..210c51d 100644 --- a/README.md +++ b/README.md @@ -75,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' From fb50aa45ecd7a104138b5d0b9ea9ac5e38f48b58 Mon Sep 17 00:00:00 2001 From: BSKY Date: Wed, 13 Nov 2019 06:48:02 +0900 Subject: [PATCH 14/37] Add initial eslint setup (#88) --- .eslintrc.json | 16 + .github/workflows/workflow.yml | 12 +- __tests__/restore.test.ts | 12 +- jest.config.js | 37 +- package-lock.json | 924 +++++++++++++++++++++++++++++++++ package.json | 12 +- src/cacheHttpClient.ts | 88 ++-- src/constants.ts | 1 + src/restore.ts | 4 +- src/save.ts | 4 +- src/utils/actionUtils.ts | 19 +- src/utils/testUtils.ts | 6 +- 12 files changed, 1044 insertions(+), 91 deletions(-) create mode 100644 .eslintrc.json 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 a8f4407..a3b8f39 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -1,4 +1,5 @@ name: Tests + on: pull_request: branches: @@ -14,14 +15,16 @@ 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' @@ -29,7 +32,12 @@ jobs: - run: npm ci - name: Prettier Format Check + if: matrix.os == 'ubuntu-latest' run: npm run format-check + - name: ESLint Check + if: matrix.os == 'ubuntu-latest' + run: npm run lint + - name: Build & Test run: npm run test diff --git a/__tests__/restore.test.ts b/__tests__/restore.test.ts index e854f6c..466b26c 100644 --- a/__tests__/restore.test.ts +++ b/__tests__/restore.test.ts @@ -1,9 +1,7 @@ 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"; @@ -107,7 +105,7 @@ test("restore with no cache found", async () => { const stateMock = jest.spyOn(core, "saveState"); const clientMock = jest.spyOn(cacheHttpClient, "getCacheEntry"); - clientMock.mockImplementation(_ => { + clientMock.mockImplementation(() => { return Promise.resolve(null); }); @@ -134,7 +132,7 @@ test("restore with server error should fail", async () => { const stateMock = jest.spyOn(core, "saveState"); const clientMock = jest.spyOn(cacheHttpClient, "getCacheEntry"); - clientMock.mockImplementation(_ => { + clientMock.mockImplementation(() => { throw new Error("HTTP Error Occurred"); }); @@ -168,7 +166,7 @@ test("restore with restore keys and no cache found", async () => { const stateMock = jest.spyOn(core, "saveState"); const clientMock = jest.spyOn(cacheHttpClient, "getCacheEntry"); - clientMock.mockImplementation(_ => { + clientMock.mockImplementation(() => { return Promise.resolve(null); }); @@ -202,7 +200,7 @@ test("restore with cache found", async () => { archiveLocation: "www.actionscache.test/download" }; const getCacheMock = jest.spyOn(cacheHttpClient, "getCacheEntry"); - getCacheMock.mockImplementation(_ => { + getCacheMock.mockImplementation(() => { return Promise.resolve(cacheEntry); }); const tempPath = "/foo/bar"; @@ -278,7 +276,7 @@ test("restore with cache found for restore key", async () => { archiveLocation: "www.actionscache.test/download" }; const getCacheMock = jest.spyOn(cacheHttpClient, "getCacheEntry"); - getCacheMock.mockImplementation(_ => { + getCacheMock.mockImplementation(() => { return Promise.resolve(cacheEntry); }); const tempPath = "/foo/bar"; diff --git a/jest.config.js b/jest.config.js index 095e90a..59548f6 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,22 +1,23 @@ -require('nock').disableNetConnect(); +require("nock").disableNetConnect(); module.exports = { - clearMocks: true, - moduleFileExtensions: ['js', 'ts'], - testEnvironment: 'node', - testMatch: ['**/*.test.ts'], - testRunner: 'jest-circus/runner', - transform: { - '^.+\\.ts$': 'ts-jest' - }, - verbose: true -} + 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) +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 26a0903..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,12 @@ "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", @@ -553,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", @@ -589,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", @@ -638,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", @@ -662,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", @@ -978,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", @@ -1024,6 +1134,12 @@ "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", @@ -1065,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", @@ -1158,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", @@ -1358,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", @@ -1377,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", @@ -1447,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", @@ -1577,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", @@ -1654,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", @@ -1675,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", @@ -1707,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", @@ -2299,6 +2764,12 @@ "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", @@ -2311,6 +2782,12 @@ "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", @@ -2349,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", @@ -2491,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", @@ -2534,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", @@ -2647,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", @@ -2659,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", @@ -2679,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", @@ -3305,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", @@ -3369,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", @@ -3473,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", @@ -3681,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", @@ -3926,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", @@ -3935,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", @@ -3986,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", @@ -4043,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", @@ -4125,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", @@ -4149,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", @@ -4167,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", @@ -4217,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", @@ -4274,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", @@ -4400,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", @@ -4421,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", @@ -4548,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", @@ -4743,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", @@ -4877,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", @@ -4892,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", @@ -4904,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", @@ -5029,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", @@ -5064,6 +5967,12 @@ "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", @@ -5195,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", @@ -5340,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 568e958..f165b86 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,9 @@ "scripts": { "build": "tsc", "test": "tsc --noEmit && jest --coverage", - "format": "prettier --write **/*.ts", - "format-check": "prettier --check **/*.ts", + "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/" }, "repository": { @@ -34,7 +35,14 @@ "@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", diff --git a/src/cacheHttpClient.ts b/src/cacheHttpClient.ts index 30b5009..b3885e4 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,21 @@ 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( + stream: NodeJS.ReadableStream, + key: string +): Promise { const cacheUrl = getCacheUrl(); const token = process.env["ACTIONS_RUNTIME_TOKEN"] || ""; const bearerCredentialHandler = new BearerCredentialHandler(token); @@ -93,32 +124,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..e77e97d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-namespace */ export namespace Inputs { export const Key = "key"; export const Path = "path"; diff --git a/src/restore.ts b/src/restore.ts index 4080098..f0b22b4 100644 --- a/src/restore.ts +++ b/src/restore.ts @@ -1,14 +1,12 @@ import * as core from "@actions/core"; import { exec } from "@actions/exec"; import * as io from "@actions/io"; - 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 { // Validate inputs, this can cause task failure let cachePath = utils.resolvePath( diff --git a/src/save.ts b/src/save.ts index 69e44cf..d3276e7 100644 --- a/src/save.ts +++ b/src/save.ts @@ -1,15 +1,13 @@ 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(); diff --git a/src/utils/actionUtils.ts b/src/utils/actionUtils.ts index 9ad0072..4732345 100644 --- a/src/utils/actionUtils.ts +++ b/src/utils/actionUtils.ts @@ -4,7 +4,6 @@ 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 { ArtifactCacheEntry } from "../contracts"; @@ -50,10 +49,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); @@ -65,14 +72,6 @@ export function getCacheState(): ArtifactCacheEntry | undefined { return (stateData && 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()); -} - export function resolvePath(filePath: string): string { if (filePath[0] === "~") { const home = os.homedir(); diff --git a/src/utils/testUtils.ts b/src/utils/testUtils.ts index 87cfc4f..b1d20d0 100644 --- a/src/utils/testUtils.ts +++ b/src/utils/testUtils.ts @@ -5,7 +5,7 @@ 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; } @@ -15,14 +15,14 @@ interface CacheInput { restoreKeys?: string[]; } -export function setInputs(input: CacheInput) { +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() { +export function clearInputs(): void { delete process.env[getInputName(Inputs.Path)]; delete process.env[getInputName(Inputs.Key)]; delete process.env[getInputName(Inputs.RestoreKeys)]; From 4657a5f525572bc97f9ba2640fe4de8efe127f89 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Tue, 12 Nov 2019 17:01:15 -0500 Subject: [PATCH 15/37] Fix lint on Windows (#89) --- .github/workflows/workflow.yml | 2 -- package.json | 6 +++--- src/constants.ts | 19 +++++++++---------- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index a3b8f39..971b0f5 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -32,11 +32,9 @@ jobs: - run: npm ci - name: Prettier Format Check - if: matrix.os == 'ubuntu-latest' run: npm run format-check - name: ESLint Check - if: matrix.os == 'ubuntu-latest' run: npm run lint - name: Build & Test diff --git a/package.json b/package.json index f165b86..5bece1c 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,9 @@ "scripts": { "build": "tsc", "test": "tsc --noEmit && jest --coverage", - "lint": "eslint '**/*.ts' --cache", - "format": "prettier --write '**/*.ts'", - "format-check": "prettier --check '**/*.ts'", + "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/" }, "repository": { diff --git a/src/constants.ts b/src/constants.ts index e77e97d..100b878 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,15 +1,14 @@ -/* eslint-disable @typescript-eslint/no-namespace */ -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" } From f0cbadd748268301b800cdf58f83b652c489e773 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Tue, 12 Nov 2019 17:48:19 -0500 Subject: [PATCH 16/37] Use cache in workflows (#90) --- .github/workflows/workflow.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/workflow.yml b/.github/workflows/workflow.yml index 971b0f5..b31fdc1 100644 --- a/.github/workflows/workflow.yml +++ b/.github/workflows/workflow.yml @@ -29,6 +29,18 @@ jobs: with: node-version: '12.x' + - name: Get npm cache directory + id: npm-cache + run: | + echo "::set-output name=dir::$(npm config get cache)" + + - uses: actions/cache@v1 + with: + path: ${{ steps.npm-cache.outputs.dir }} + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + - run: npm ci - name: Prettier Format Check From 50a2fdee6fa08b2f61cca3b302ffea8e870b2627 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Wed, 13 Nov 2019 10:18:47 -0500 Subject: [PATCH 17/37] Update yarn cache example (#70) * Update yarn cache example * Update examples.md Co-Authored-By: Eric Taylor --- examples.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/examples.md b/examples.md index 2d5a8a7..bf3633b 100644 --- a/examples.md +++ b/examples.md @@ -112,12 +112,17 @@ For npm, cache files are stored in `~/.npm` on Posix, or `%AppData%/npm-cache` o ``` ## 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: ~/.cache/yarn - key: ${{ runner.os }}-yarn-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }} + path: ${{ steps.yarn-cache.outputs.dir }} + key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} restore-keys: | ${{ runner.os }}-yarn- ``` From b7d83b40950d5a8944106e0b29750090dc13fad8 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Wed, 13 Nov 2019 10:54:39 -0500 Subject: [PATCH 18/37] Provide better errors for unsupported event types (#68) * Validate event type during restore * PR Feedback * Format * Linting --- __tests__/restore.test.ts | 104 +++++++++++++++++++++++++++++++++++++- src/constants.ts | 6 +++ src/restore.ts | 12 ++++- src/utils/actionUtils.ts | 15 +++++- 4 files changed, 134 insertions(+), 3 deletions(-) diff --git a/__tests__/restore.test.ts b/__tests__/restore.test.ts index 466b26c..191f804 100644 --- a/__tests__/restore.test.ts +++ b/__tests__/restore.test.ts @@ -3,7 +3,7 @@ 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 { Events, Inputs } from "../src/constants"; import { ArtifactCacheEntry } from "../src/contracts"; import run from "../src/restore"; import * as actionUtils from "../src/utils/actionUtils"; @@ -26,12 +26,38 @@ beforeAll(() => { } ); + 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 () => { @@ -255,6 +281,82 @@ test("restore with cache found", async () => { 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: "https://www.example.com/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 tarArchivePath = IS_WINDOWS + ? archivePath.replace(/\\/g, "/") + : archivePath; + const tarCachePath = IS_WINDOWS ? cachePath.replace(/\\/g, "/") : cachePath; + const args = IS_WINDOWS ? ["-xz", "--force-local"] : ["-xz"]; + args.push(...["-f", tarArchivePath, "-C", tarCachePath]); + + 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-"; diff --git a/src/constants.ts b/src/constants.ts index 100b878..5f26e8c 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -12,3 +12,9 @@ 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 f0b22b4..0af62d4 100644 --- a/src/restore.ts +++ b/src/restore.ts @@ -3,12 +3,22 @@ import { exec } from "@actions/exec"; import * as io from "@actions/io"; 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(): Promise { try { // Validate inputs, this can cause task failure + 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.` + ); + } + let cachePath = utils.resolvePath( core.getInput(Inputs.Path, { required: true }) ); diff --git a/src/utils/actionUtils.ts b/src/utils/actionUtils.ts index 4732345..15b73fd 100644 --- a/src/utils/actionUtils.ts +++ b/src/utils/actionUtils.ts @@ -4,7 +4,8 @@ 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 @@ -83,3 +84,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); +} From 7e7aef2963f394b513de55deee2b805fae42ed3a Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Wed, 13 Nov 2019 10:55:05 -0500 Subject: [PATCH 19/37] Add pip examples (#86) --- examples.md | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/examples.md b/examples.md index bf3633b..b05e531 100644 --- a/examples.md +++ b/examples.md @@ -8,6 +8,7 @@ - [Node - npm](#node---npm) - [Node - Yarn](#node---yarn) - [PHP - Composer](#php---composer) +- [Python - pip](#python---pip) - [Ruby - Gem](#ruby---gem) - [Rust - Cargo](#rust---cargo) - [Swift, Objective-C - Carthage](#swift-objective-c---carthage) @@ -142,6 +143,72 @@ The yarn cache directory will depend on your operating system and version of `ya ${{ 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/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ 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 ```yaml From bb828da54c148048dd17899ba9fda624811cfb43 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Wed, 13 Nov 2019 11:00:46 -0500 Subject: [PATCH 20/37] Format cache size and display on info (#85) --- __tests__/restore.test.ts | 4 +++- src/restore.ts | 6 +++++- src/save.ts | 4 +++- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/__tests__/restore.test.ts b/__tests__/restore.test.ts index 191f804..f8c4cb3 100644 --- a/__tests__/restore.test.ts +++ b/__tests__/restore.test.ts @@ -319,7 +319,7 @@ test("restore with a pull request event and cache found", async () => { const setCacheStateMock = jest.spyOn(actionUtils, "setCacheState"); const downloadCacheMock = jest.spyOn(cacheHttpClient, "downloadCache"); - const fileSize = 142; + const fileSize = 62915000; const getArchiveFileSizeMock = jest .spyOn(actionUtils, "getArchiveFileSize") .mockReturnValue(fileSize); @@ -336,6 +336,7 @@ test("restore with a pull request event and cache found", async () => { 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"; @@ -412,6 +413,7 @@ test("restore with cache found for restore key", async () => { 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"; diff --git a/src/restore.ts b/src/restore.ts index 0af62d4..6936652 100644 --- a/src/restore.ts +++ b/src/restore.ts @@ -80,7 +80,11 @@ async function run(): Promise { await cacheHttpClient.downloadCache(cacheEntry, archivePath); const archiveFileSize = utils.getArchiveFileSize(archivePath); - core.debug(`File Size: ${archiveFileSize}`); + core.info( + `Cache Size: ~${Math.round( + archiveFileSize / (1024 * 1024) + )} MB (${archiveFileSize} B)` + ); io.mkdirP(cachePath); diff --git a/src/save.ts b/src/save.ts index d3276e7..c4d5d61 100644 --- a/src/save.ts +++ b/src/save.ts @@ -57,7 +57,9 @@ async function run(): Promise { 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; } From c0584c42d1437407d685968782627668bb9a2024 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Wed, 13 Nov 2019 16:13:00 -0500 Subject: [PATCH 21/37] Add unit tests for actionUtils (#93) * Add unit tests for actionUtils * Fix file size on ubuntu and test name * Remove unused import --- __tests__/__fixtures__/helloWorld.txt | 1 + __tests__/actionUtils.test.ts | 226 ++++++++++++++++++++++++++ __tests__/restore.test.ts | 2 +- src/utils/actionUtils.ts | 6 +- 4 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 __tests__/__fixtures__/helloWorld.txt create mode 100644 __tests__/actionUtils.test.ts 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__/restore.test.ts b/__tests__/restore.test.ts index f8c4cb3..d2f7dd8 100644 --- a/__tests__/restore.test.ts +++ b/__tests__/restore.test.ts @@ -299,7 +299,7 @@ test("restore with a pull request event and cache found", async () => { const cacheEntry: ArtifactCacheEntry = { cacheKey: key, scope: "refs/heads/master", - archiveLocation: "https://www.example.com/download" + archiveLocation: "www.actionscache.test/download" }; const getCacheMock = jest.spyOn(cacheHttpClient, "getCacheEntry"); getCacheMock.mockImplementation(() => { diff --git a/src/utils/actionUtils.ts b/src/utils/actionUtils.ts index 15b73fd..ba5b2ea 100644 --- a/src/utils/actionUtils.ts +++ b/src/utils/actionUtils.ts @@ -70,7 +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; + } + + return undefined; } export function resolvePath(filePath: string): string { From 8d14a2150b09e885a026191fd0643f4746224cc0 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Thu, 14 Nov 2019 17:14:16 -0500 Subject: [PATCH 22/37] Add unit tests for save (#98) * Clean up args and arrange imports * Arrange args in restore tests * Add unit tests for save * Use const instead of let (linting) --- __tests__/restore.test.ts | 48 +++--- __tests__/save.test.ts | 329 ++++++++++++++++++++++++++++++++++++++ src/cacheHttpClient.ts | 6 +- src/restore.ts | 22 +-- src/save.ts | 29 ++-- 5 files changed, 390 insertions(+), 44 deletions(-) create mode 100644 __tests__/save.test.ts diff --git a/__tests__/restore.test.ts b/__tests__/restore.test.ts index d2f7dd8..1919e30 100644 --- a/__tests__/restore.test.ts +++ b/__tests__/restore.test.ts @@ -263,12 +263,16 @@ test("restore with cache found", async () => { expect(mkdirMock).toHaveBeenCalledWith(cachePath); const IS_WINDOWS = process.platform === "win32"; - const tarArchivePath = IS_WINDOWS - ? archivePath.replace(/\\/g, "/") - : archivePath; - const tarCachePath = IS_WINDOWS ? cachePath.replace(/\\/g, "/") : cachePath; - const args = IS_WINDOWS ? ["-xz", "--force-local"] : ["-xz"]; - args.push(...["-f", tarArchivePath, "-C", tarCachePath]); + 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); @@ -340,12 +344,16 @@ test("restore with a pull request event and cache found", async () => { expect(mkdirMock).toHaveBeenCalledWith(cachePath); const IS_WINDOWS = process.platform === "win32"; - const tarArchivePath = IS_WINDOWS - ? archivePath.replace(/\\/g, "/") - : archivePath; - const tarCachePath = IS_WINDOWS ? cachePath.replace(/\\/g, "/") : cachePath; - const args = IS_WINDOWS ? ["-xz", "--force-local"] : ["-xz"]; - args.push(...["-f", tarArchivePath, "-C", tarCachePath]); + 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); @@ -417,12 +425,16 @@ test("restore with cache found for restore key", async () => { expect(mkdirMock).toHaveBeenCalledWith(cachePath); const IS_WINDOWS = process.platform === "win32"; - const tarArchivePath = IS_WINDOWS - ? archivePath.replace(/\\/g, "/") - : archivePath; - const tarCachePath = IS_WINDOWS ? cachePath.replace(/\\/g, "/") : cachePath; - const args = IS_WINDOWS ? ["-xz", "--force-local"] : ["-xz"]; - args.push(...["-f", tarArchivePath, "-C", tarCachePath]); + 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); 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/src/cacheHttpClient.ts b/src/cacheHttpClient.ts index b3885e4..e448157 100644 --- a/src/cacheHttpClient.ts +++ b/src/cacheHttpClient.ts @@ -93,9 +93,11 @@ export async function downloadCache( } export async function saveCache( - stream: NodeJS.ReadableStream, - key: string + 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); diff --git a/src/restore.ts b/src/restore.ts index 6936652..5d9da4a 100644 --- a/src/restore.ts +++ b/src/restore.ts @@ -19,7 +19,7 @@ async function run(): Promise { ); } - let cachePath = utils.resolvePath( + const cachePath = utils.resolvePath( core.getInput(Inputs.Path, { required: true }) ); core.debug(`Cache Path: ${cachePath}`); @@ -67,7 +67,7 @@ async function run(): Promise { return; } - let archivePath = path.join( + const archivePath = path.join( await utils.createTempDirectory(), "cache.tgz" ); @@ -90,15 +90,17 @@ async function run(): Promise { // 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}`); diff --git a/src/save.ts b/src/save.ts index c4d5d61..d660064 100644 --- a/src/save.ts +++ b/src/save.ts @@ -1,7 +1,6 @@ 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"; @@ -25,12 +24,12 @@ async function run(): Promise { 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" ); @@ -38,22 +37,25 @@ async function run(): Promise { // 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( @@ -64,8 +66,7 @@ async function run(): Promise { return; } - const stream = fs.createReadStream(archivePath); - await cacheHttpClient.saveCache(stream, primaryKey); + await cacheHttpClient.saveCache(primaryKey, archivePath); } catch (error) { core.warning(error.message); } From 84b3b283f038371f6f9ab774cf8fbcb7f49edce3 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Fri, 15 Nov 2019 10:25:57 -0500 Subject: [PATCH 23/37] Await io mkdirP (#100) --- src/restore.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/restore.ts b/src/restore.ts index 5d9da4a..87a2684 100644 --- a/src/restore.ts +++ b/src/restore.ts @@ -86,7 +86,8 @@ async function run(): Promise { )} MB (${archiveFileSize} B)` ); - io.mkdirP(cachePath); + // 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] From 92ae3b63f8569fb2d61daa8aaeb94b640ecbf66f Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Fri, 15 Nov 2019 15:04:12 -0500 Subject: [PATCH 24/37] Update badge link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 210c51d..1278fae 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ This GitHub Action allows caching dependencies and build outputs to improve workflow execution time. -GitHub Actions status +GitHub Actions status ## Documentation From d9fe1b81f95b9cacdb8f2eec1e0059fac59b1133 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Tue, 19 Nov 2019 11:55:11 -0500 Subject: [PATCH 25/37] Release 1.0.2 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 605ecc4..9821cb1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cache", - "version": "1.0.1", + "version": "1.0.2", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 5bece1c..dd09d47 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cache", - "version": "1.0.1", + "version": "1.0.2", "private": true, "description": "Cache dependencies and build outputs", "main": "dist/restore/index.js", From 639f9d8b816219a3f0114177696d3c69c4fc17f6 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Thu, 21 Nov 2019 14:37:32 -0500 Subject: [PATCH 26/37] Mask download URL in logs (#110) --- src/cacheHttpClient.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cacheHttpClient.ts b/src/cacheHttpClient.ts index e448157..8a2014f 100644 --- a/src/cacheHttpClient.ts +++ b/src/cacheHttpClient.ts @@ -61,11 +61,12 @@ export async function getCacheEntry( throw new Error(`Cache service responded with ${response.statusCode}`); } const cacheResult = response.result; - core.debug(`Cache Result:`); - core.debug(JSON.stringify(cacheResult)); if (!cacheResult || !cacheResult.archiveLocation) { throw new Error("Cache not found."); } + core.setSecret(cacheResult.archiveLocation); + core.debug(`Cache Result:`); + core.debug(JSON.stringify(cacheResult)); return cacheResult; } From 95c17983692a3b9f2b3b50b7372247f6fe140cec Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Thu, 21 Nov 2019 14:37:54 -0500 Subject: [PATCH 27/37] Remove validation failures and warning annotations (#108) * Update warnings behavior * Add void return type --- __tests__/actionUtils.test.ts | 10 ++++++ __tests__/restore.test.ts | 22 +++++--------- __tests__/save.test.ts | 57 ++++++++++++++++++++++++----------- package-lock.json | 2 +- package.json | 2 +- src/restore.ts | 5 +-- src/save.ts | 19 +++++++++--- src/utils/actionUtils.ts | 5 +++ 8 files changed, 82 insertions(+), 40 deletions(-) diff --git a/__tests__/actionUtils.test.ts b/__tests__/actionUtils.test.ts index 4688b5d..f46d65d 100644 --- a/__tests__/actionUtils.test.ts +++ b/__tests__/actionUtils.test.ts @@ -162,6 +162,16 @@ test("getCacheState with valid state", () => { expect(getStateMock).toHaveBeenCalledTimes(1); }); +test("logWarning logs a message with a warning prefix", () => { + const message = "A warning occurred."; + + const infoMock = jest.spyOn(core, "info"); + + actionUtils.logWarning(message); + + expect(infoMock).toHaveBeenCalledWith(`[warning]${message}`); +}); + test("isValidEvent returns false for unknown event", () => { const event = "foo"; process.env[Events.Key] = event; diff --git a/__tests__/restore.test.ts b/__tests__/restore.test.ts index 1919e30..78b00ec 100644 --- a/__tests__/restore.test.ts +++ b/__tests__/restore.test.ts @@ -50,14 +50,16 @@ afterEach(() => { delete process.env[Events.Key]; }); -test("restore with invalid event", async () => { +test("restore with invalid event outputs warning", async () => { + const logWarningMock = jest.spyOn(actionUtils, "logWarning"); const failedMock = jest.spyOn(core, "setFailed"); const invalidEvent = "commit_comment"; process.env[Events.Key] = invalidEvent; await run(); - expect(failedMock).toHaveBeenCalledWith( + expect(logWarningMock).toHaveBeenCalledWith( `Event Validation Error: The event type ${invalidEvent} is not supported. Only push, pull_request events are supported at this time.` ); + expect(failedMock).toHaveBeenCalledTimes(0); }); test("restore with no path should fail", async () => { @@ -126,7 +128,6 @@ test("restore with no cache found", async () => { }); const infoMock = jest.spyOn(core, "info"); - const warningMock = jest.spyOn(core, "warning"); const failedMock = jest.spyOn(core, "setFailed"); const stateMock = jest.spyOn(core, "saveState"); @@ -138,7 +139,6 @@ test("restore with no cache found", async () => { await run(); expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); - expect(warningMock).toHaveBeenCalledTimes(0); expect(failedMock).toHaveBeenCalledTimes(0); expect(infoMock).toHaveBeenCalledWith( @@ -153,7 +153,7 @@ test("restore with server error should fail", async () => { key }); - const warningMock = jest.spyOn(core, "warning"); + const logWarningMock = jest.spyOn(actionUtils, "logWarning"); const failedMock = jest.spyOn(core, "setFailed"); const stateMock = jest.spyOn(core, "saveState"); @@ -168,8 +168,8 @@ test("restore with server error should fail", async () => { expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); - expect(warningMock).toHaveBeenCalledTimes(1); - expect(warningMock).toHaveBeenCalledWith("HTTP Error Occurred"); + expect(logWarningMock).toHaveBeenCalledTimes(1); + expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred"); expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); expect(setCacheHitOutputMock).toHaveBeenCalledWith(false); @@ -187,7 +187,6 @@ test("restore with restore keys and no cache found", async () => { }); const infoMock = jest.spyOn(core, "info"); - const warningMock = jest.spyOn(core, "warning"); const failedMock = jest.spyOn(core, "setFailed"); const stateMock = jest.spyOn(core, "saveState"); @@ -199,7 +198,6 @@ test("restore with restore keys and no cache found", async () => { await run(); expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key); - expect(warningMock).toHaveBeenCalledTimes(0); expect(failedMock).toHaveBeenCalledTimes(0); expect(infoMock).toHaveBeenCalledWith( @@ -216,7 +214,6 @@ test("restore with cache found", async () => { }); const infoMock = jest.spyOn(core, "info"); - const warningMock = jest.spyOn(core, "warning"); const failedMock = jest.spyOn(core, "setFailed"); const stateMock = jest.spyOn(core, "saveState"); @@ -281,7 +278,6 @@ test("restore with cache found", async () => { expect(setCacheHitOutputMock).toHaveBeenCalledWith(true); expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`); - expect(warningMock).toHaveBeenCalledTimes(0); expect(failedMock).toHaveBeenCalledTimes(0); }); @@ -296,7 +292,6 @@ test("restore with a pull request event and cache found", async () => { 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"); @@ -362,7 +357,6 @@ test("restore with a pull request event and cache found", async () => { expect(setCacheHitOutputMock).toHaveBeenCalledWith(true); expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`); - expect(warningMock).toHaveBeenCalledTimes(0); expect(failedMock).toHaveBeenCalledTimes(0); }); @@ -377,7 +371,6 @@ test("restore with cache found for restore key", async () => { }); const infoMock = jest.spyOn(core, "info"); - const warningMock = jest.spyOn(core, "warning"); const failedMock = jest.spyOn(core, "setFailed"); const stateMock = jest.spyOn(core, "saveState"); @@ -445,6 +438,5 @@ test("restore with cache found for restore key", async () => { 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 index 67b13d2..89657c4 100644 --- a/__tests__/save.test.ts +++ b/__tests__/save.test.ts @@ -3,7 +3,7 @@ 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 { Events, Inputs } from "../src/constants"; import { ArtifactCacheEntry } from "../src/contracts"; import run from "../src/save"; import * as actionUtils from "../src/utils/actionUtils"; @@ -32,6 +32,16 @@ beforeAll(() => { } ); + 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(actionUtils, "resolvePath").mockImplementation(filePath => { return path.resolve(filePath); }); @@ -45,12 +55,29 @@ beforeAll(() => { }); }); +beforeEach(() => { + process.env[Events.Key] = Events.Push; +}); + afterEach(() => { testUtils.clearInputs(); + delete process.env[Events.Key]; +}); + +test("save with invalid event outputs warning", async () => { + const logWarningMock = jest.spyOn(actionUtils, "logWarning"); + const failedMock = jest.spyOn(core, "setFailed"); + const invalidEvent = "commit_comment"; + process.env[Events.Key] = invalidEvent; + await run(); + expect(logWarningMock).toHaveBeenCalledWith( + `Event Validation Error: The event type ${invalidEvent} is not supported. Only push, pull_request events are supported at this time.` + ); + expect(failedMock).toHaveBeenCalledTimes(0); }); test("save with no primary key in state outputs warning", async () => { - const warningMock = jest.spyOn(core, "warning"); + const logWarningMock = jest.spyOn(actionUtils, "logWarning"); const failedMock = jest.spyOn(core, "setFailed"); const cacheEntry: ArtifactCacheEntry = { @@ -72,16 +99,15 @@ test("save with no primary key in state outputs warning", async () => { await run(); - expect(warningMock).toHaveBeenCalledWith( + expect(logWarningMock).toHaveBeenCalledWith( `Error retrieving key from state.` ); - expect(warningMock).toHaveBeenCalledTimes(1); + expect(logWarningMock).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"; @@ -112,12 +138,11 @@ test("save with exact match returns early", async () => { 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 logWarningMock = jest.spyOn(actionUtils, "logWarning"); const failedMock = jest.spyOn(core, "setFailed"); const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; @@ -140,15 +165,15 @@ test("save with missing input outputs warning", async () => { await run(); - expect(warningMock).toHaveBeenCalledWith( + expect(logWarningMock).toHaveBeenCalledWith( "Input required and not supplied: path" ); - expect(warningMock).toHaveBeenCalledTimes(1); + expect(logWarningMock).toHaveBeenCalledTimes(1); expect(failedMock).toHaveBeenCalledTimes(0); }); test("save with large cache outputs warning", async () => { - const warningMock = jest.spyOn(core, "warning"); + const logWarningMock = jest.spyOn(actionUtils, "logWarning"); const failedMock = jest.spyOn(core, "setFailed"); const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; @@ -200,8 +225,8 @@ test("save with large cache outputs warning", async () => { expect(execMock).toHaveBeenCalledTimes(1); expect(execMock).toHaveBeenCalledWith(`"tar"`, args); - expect(warningMock).toHaveBeenCalledTimes(1); - expect(warningMock).toHaveBeenCalledWith( + expect(logWarningMock).toHaveBeenCalledTimes(1); + expect(logWarningMock).toHaveBeenCalledWith( "Cache size of ~1024 MB (1073741824 B) is over the 400MB limit, not saving cache." ); @@ -209,7 +234,7 @@ test("save with large cache outputs warning", async () => { }); test("save with server error outputs warning", async () => { - const warningMock = jest.spyOn(core, "warning"); + const logWarningMock = jest.spyOn(actionUtils, "logWarning"); const failedMock = jest.spyOn(core, "setFailed"); const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43"; @@ -265,14 +290,13 @@ test("save with server error outputs warning", async () => { expect(saveCacheMock).toHaveBeenCalledTimes(1); expect(saveCacheMock).toHaveBeenCalledWith(primaryKey, archivePath); - expect(warningMock).toHaveBeenCalledTimes(1); - expect(warningMock).toHaveBeenCalledWith("HTTP Error Occurred"); + expect(logWarningMock).toHaveBeenCalledTimes(1); + expect(logWarningMock).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"; @@ -324,6 +348,5 @@ test("save with valid inputs uploads a cache", async () => { expect(saveCacheMock).toHaveBeenCalledTimes(1); expect(saveCacheMock).toHaveBeenCalledWith(primaryKey, archivePath); - expect(warningMock).toHaveBeenCalledTimes(0); expect(failedMock).toHaveBeenCalledTimes(0); }); diff --git a/package-lock.json b/package-lock.json index 9821cb1..2e8413e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cache", - "version": "1.0.2", + "version": "1.0.3", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index dd09d47..42fbdbe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cache", - "version": "1.0.2", + "version": "1.0.3", "private": true, "description": "Cache dependencies and build outputs", "main": "dist/restore/index.js", diff --git a/src/restore.ts b/src/restore.ts index 87a2684..15570cd 100644 --- a/src/restore.ts +++ b/src/restore.ts @@ -10,13 +10,14 @@ async function run(): Promise { try { // Validate inputs, this can cause task failure if (!utils.isValidEvent()) { - core.setFailed( + utils.logWarning( `Event Validation Error: The event type ${ process.env[Events.Key] } is not supported. Only ${utils .getSupportedEvents() .join(", ")} events are supported at this time.` ); + return; } const cachePath = utils.resolvePath( @@ -118,7 +119,7 @@ async function run(): Promise { `Cache restored from key: ${cacheEntry && cacheEntry.cacheKey}` ); } catch (error) { - core.warning(error.message); + utils.logWarning(error.message); utils.setCacheHitOutput(false); } } catch (error) { diff --git a/src/save.ts b/src/save.ts index d660064..21f32d3 100644 --- a/src/save.ts +++ b/src/save.ts @@ -3,17 +3,28 @@ import { exec } from "@actions/exec"; import * as io from "@actions/io"; 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(): Promise { try { + if (!utils.isValidEvent()) { + utils.logWarning( + `Event Validation Error: The event type ${ + process.env[Events.Key] + } is not supported. Only ${utils + .getSupportedEvents() + .join(", ")} events are supported at this time.` + ); + return; + } + const state = utils.getCacheState(); // Inputs are re-evaluted before the post action, so we want the original key used for restore const primaryKey = core.getState(State.CacheKey); if (!primaryKey) { - core.warning(`Error retrieving key from state.`); + utils.logWarning(`Error retrieving key from state.`); return; } @@ -58,7 +69,7 @@ async function run(): Promise { const archiveFileSize = utils.getArchiveFileSize(archivePath); core.debug(`File Size: ${archiveFileSize}`); if (archiveFileSize > fileSizeLimit) { - core.warning( + utils.logWarning( `Cache size of ~${Math.round( archiveFileSize / (1024 * 1024) )} MB (${archiveFileSize} B) is over the 400MB limit, not saving cache.` @@ -68,7 +79,7 @@ async function run(): Promise { await cacheHttpClient.saveCache(primaryKey, archivePath); } catch (error) { - core.warning(error.message); + utils.logWarning(error.message); } } diff --git a/src/utils/actionUtils.ts b/src/utils/actionUtils.ts index ba5b2ea..f6369fb 100644 --- a/src/utils/actionUtils.ts +++ b/src/utils/actionUtils.ts @@ -77,6 +77,11 @@ export function getCacheState(): ArtifactCacheEntry | undefined { return undefined; } +export function logWarning(message: string): void { + const warningPrefix = "[warning]"; + core.info(`${warningPrefix}${message}`); +} + export function resolvePath(filePath: string): string { if (filePath[0] === "~") { const home = os.homedir(); From 3d01b4eb53c7cbc3df0c28cb47cc45754127fe57 Mon Sep 17 00:00:00 2001 From: Evan Cloutier <9866928+evancloutier@users.noreply.github.com> Date: Sat, 23 Nov 2019 14:13:50 -0500 Subject: [PATCH 28/37] Update Ruby example in documentation to specify bundler path (#113) * Update Ruby example to specify bundler path * Fix spacing --- examples.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples.md b/examples.md index b05e531..716f2bb 100644 --- a/examples.md +++ b/examples.md @@ -219,6 +219,14 @@ Replace `~/.cache/pip` with the correct `path` if not using Ubuntu. restore-keys: | ${{ runner.os }}-gem- ``` +When dependencies are installed later in the workflow, we must specify the same path for the bundler. + +```yaml +- name: Bundle install + run: | + bundle config path vendor/bundle + bundle install --jobs 4 --retry 3 +``` ## Rust - Cargo From 4809f4ada49f99628c256b84f23798ced8214a3c Mon Sep 17 00:00:00 2001 From: Jon Pugh Date: Sat, 7 Dec 2019 18:25:23 -0500 Subject: [PATCH 29/37] Add list of implementation examples. (#116) More visibility into the samples by having it on the main README. Easier to see, better SEO. --- README.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1278fae..70ae416 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,26 @@ jobs: run: /primes.sh -d prime-numbers ``` -## Ecosystem Examples +## Implementation Examples + +Every programming language and framework has it's own way of caching. + +See [Examples](examples.md) for a list of `actions/cache` implementations for use with: + +- [C# - Nuget](./examples.md#c---nuget) +- [Elixir - Mix](./examples.md#elixir---mix) +- [Go - Modules](./examples.md#go---modules) +- [Java - Gradle](./examples.md#java---gradle) +- [Java - Maven](./examples.md#java---maven) +- [Node - npm](./examples.md#node---npm) +- [Node - Yarn](./examples.md#node---yarn) +- [PHP - Composer](./examples.md#php---composer) +- [Python - pip](./examples.md#python---pip) +- [Ruby - Gem](./examples.md#ruby---gem) +- [Rust - Cargo](./examples.md#rust---cargo) +- [Swift, Objective-C - Carthage](./examples.md#swift-objective-c---carthage) +- [Swift, Objective-C - CocoaPods](./examples.md#swift-objective-c---cocoapods) -See [Examples](examples.md) ## Cache Limits From 002d3a77f4c5d6c87f0444cd90c0464892209ff7 Mon Sep 17 00:00:00 2001 From: Nogic <24802730+nogic1008@users.noreply.github.com> Date: Tue, 10 Dec 2019 09:21:47 +0900 Subject: [PATCH 30/37] Use Personal Cache Folder in C# Example Ref: #115 --- examples.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/examples.md b/examples.md index 716f2bb..cc5da9e 100644 --- a/examples.md +++ b/examples.md @@ -1,6 +1,6 @@ # Examples -- [C# - Nuget](#c---nuget) +- [C# - NuGet](#c---nuget) - [Elixir - Mix](#elixir---mix) - [Go - Modules](#go---modules) - [Java - Gradle](#java---gradle) @@ -14,16 +14,21 @@ - [Swift, Objective-C - Carthage](#swift-objective-c---carthage) - [Swift, Objective-C - CocoaPods](#swift-objective-c---cocoapods) -## C# - Nuget +## C# - NuGet Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/package-references-in-project-files#locking-dependencies): ```yaml -- uses: actions/cache@v1 - with: - path: ~/.nuget/packages - key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} - restore-keys: | - ${{ runner.os }}-nuget- +env: + # Use personal cache folder because global cache may have huge packages like Xamarin. + # Learn more, see issue #115. + NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages +steps: + - uses: actions/cache@v1 + with: + path: ${{ github.workspace }}/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget- ``` ## Elixir - Mix From 0188dffc5a9973623bf3323af68a20d0ea5d4b4c Mon Sep 17 00:00:00 2001 From: Nogic <24802730+nogic1008@users.noreply.github.com> Date: Fri, 13 Dec 2019 10:03:43 +0900 Subject: [PATCH 31/37] Revert original C# Example * Treat "Use Personal Cache Folder" way as another C# example * Describe the situation in which another example should be used --- examples.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/examples.md b/examples.md index cc5da9e..7552281 100644 --- a/examples.md +++ b/examples.md @@ -17,10 +17,20 @@ ## C# - NuGet Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/package-references-in-project-files#locking-dependencies): +```yaml +- uses: actions/cache@v1 + with: + path: ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} + restore-keys: | + ${{ runner.os }}-nuget- +``` + +Depending on the environment, huge packages might be pre-installed in the global cache folder. +If you do not want to include them, consider to move the cache folder like below. +>Note: This workflow does not work for projects that require files to be placed in the global. ```yaml env: - # Use personal cache folder because global cache may have huge packages like Xamarin. - # Learn more, see issue #115. NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages steps: - uses: actions/cache@v1 From 3854a40aee6b66135b974c5a63118c69b6c0d2d0 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Fri, 13 Dec 2019 17:24:37 -0500 Subject: [PATCH 32/37] Use BSD tar on windows (#126) * Use BSD tar on windows * Linting * Fallback to which tar if no system tar * Fix formatting * Bump prettier and typescript --- __tests__/restore.test.ts | 72 +++++++------------------------------ __tests__/save.test.ts | 74 ++++++++------------------------------- __tests__/tar.test.ts | 58 ++++++++++++++++++++++++++++++ package-lock.json | 12 +++---- package.json | 4 +-- src/restore.ts | 25 ++----------- src/save.ts | 22 ++---------- src/tar.ts | 47 +++++++++++++++++++++++++ 8 files changed, 143 insertions(+), 171 deletions(-) create mode 100644 __tests__/tar.test.ts create mode 100644 src/tar.ts diff --git a/__tests__/restore.test.ts b/__tests__/restore.test.ts index 78b00ec..15e0ba5 100644 --- a/__tests__/restore.test.ts +++ b/__tests__/restore.test.ts @@ -1,18 +1,16 @@ 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 tar from "../src/tar"; 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"); +jest.mock("../src/tar"); +jest.mock("../src/utils/actionUtils"); beforeAll(() => { jest.spyOn(actionUtils, "resolvePath").mockImplementation(filePath => { @@ -35,10 +33,6 @@ beforeAll(() => { const actualUtils = jest.requireActual("../src/utils/actionUtils"); return actualUtils.getSupportedEvents(); }); - - jest.spyOn(io, "which").mockImplementation(tool => { - return Promise.resolve(tool); - }); }); beforeEach(() => { @@ -245,8 +239,7 @@ test("restore with cache found", async () => { .spyOn(actionUtils, "getArchiveFileSize") .mockReturnValue(fileSize); - const mkdirMock = jest.spyOn(io, "mkdirP"); - const execMock = jest.spyOn(exec, "exec"); + const extractTarMock = jest.spyOn(tar, "extractTar"); const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput"); await run(); @@ -257,22 +250,9 @@ test("restore with cache found", async () => { 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(extractTarMock).toHaveBeenCalledTimes(1); + expect(extractTarMock).toHaveBeenCalledWith(archivePath, cachePath); expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); expect(setCacheHitOutputMock).toHaveBeenCalledWith(true); @@ -323,8 +303,7 @@ test("restore with a pull request event and cache found", async () => { .spyOn(actionUtils, "getArchiveFileSize") .mockReturnValue(fileSize); - const mkdirMock = jest.spyOn(io, "mkdirP"); - const execMock = jest.spyOn(exec, "exec"); + const extractTarMock = jest.spyOn(tar, "extractTar"); const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput"); await run(); @@ -336,22 +315,9 @@ test("restore with a pull request event and cache found", async () => { 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(extractTarMock).toHaveBeenCalledTimes(1); + expect(extractTarMock).toHaveBeenCalledWith(archivePath, cachePath); expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); expect(setCacheHitOutputMock).toHaveBeenCalledWith(true); @@ -402,8 +368,7 @@ test("restore with cache found for restore key", async () => { .spyOn(actionUtils, "getArchiveFileSize") .mockReturnValue(fileSize); - const mkdirMock = jest.spyOn(io, "mkdirP"); - const execMock = jest.spyOn(exec, "exec"); + const extractTarMock = jest.spyOn(tar, "extractTar"); const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput"); await run(); @@ -415,22 +380,9 @@ test("restore with cache found for restore key", async () => { 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(extractTarMock).toHaveBeenCalledTimes(1); + expect(extractTarMock).toHaveBeenCalledWith(archivePath, cachePath); expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1); expect(setCacheHitOutputMock).toHaveBeenCalledWith(false); diff --git a/__tests__/save.test.ts b/__tests__/save.test.ts index 89657c4..ba76cda 100644 --- a/__tests__/save.test.ts +++ b/__tests__/save.test.ts @@ -1,19 +1,17 @@ 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/save"; +import * as tar from "../src/tar"; 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"); +jest.mock("../src/tar"); +jest.mock("../src/utils/actionUtils"); beforeAll(() => { jest.spyOn(core, "getInput").mockImplementation((name, options) => { @@ -49,10 +47,6 @@ beforeAll(() => { jest.spyOn(actionUtils, "createTempDirectory").mockImplementation(() => { return Promise.resolve("/foo/bar"); }); - - jest.spyOn(io, "which").mockImplementation(tool => { - return Promise.resolve(tool); - }); }); beforeEach(() => { @@ -128,7 +122,7 @@ test("save with exact match returns early", async () => { return primaryKey; }); - const execMock = jest.spyOn(exec, "exec"); + const createTarMock = jest.spyOn(tar, "createTar"); await run(); @@ -136,7 +130,7 @@ test("save with exact match returns early", async () => { `Cache hit occurred on the primary key ${primaryKey}, not saving cache.` ); - expect(execMock).toHaveBeenCalledTimes(0); + expect(createTarMock).toHaveBeenCalledTimes(0); expect(failedMock).toHaveBeenCalledTimes(0); }); @@ -198,7 +192,7 @@ test("save with large cache outputs warning", async () => { const cachePath = path.resolve(inputPath); testUtils.setInput(Inputs.Path, inputPath); - const execMock = jest.spyOn(exec, "exec"); + const createTarMock = jest.spyOn(tar, "createTar"); const cacheSize = 1024 * 1024 * 1024; //~1GB, over the 400MB limit jest.spyOn(actionUtils, "getArchiveFileSize").mockImplementationOnce(() => { @@ -209,21 +203,8 @@ test("save with large cache outputs warning", async () => { 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(createTarMock).toHaveBeenCalledTimes(1); + expect(createTarMock).toHaveBeenCalledWith(archivePath, cachePath); expect(logWarningMock).toHaveBeenCalledTimes(1); expect(logWarningMock).toHaveBeenCalledWith( @@ -259,7 +240,7 @@ test("save with server error outputs warning", async () => { const cachePath = path.resolve(inputPath); testUtils.setInput(Inputs.Path, inputPath); - const execMock = jest.spyOn(exec, "exec"); + const createTarMock = jest.spyOn(tar, "createTar"); const saveCacheMock = jest .spyOn(cacheHttpClient, "saveCache") @@ -271,21 +252,8 @@ test("save with server error outputs warning", async () => { 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(createTarMock).toHaveBeenCalledTimes(1); + expect(createTarMock).toHaveBeenCalledWith(archivePath, cachePath); expect(saveCacheMock).toHaveBeenCalledTimes(1); expect(saveCacheMock).toHaveBeenCalledWith(primaryKey, archivePath); @@ -321,29 +289,15 @@ test("save with valid inputs uploads a cache", async () => { const cachePath = path.resolve(inputPath); testUtils.setInput(Inputs.Path, inputPath); - const execMock = jest.spyOn(exec, "exec"); - + const createTarMock = jest.spyOn(tar, "createTar"); 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(createTarMock).toHaveBeenCalledTimes(1); + expect(createTarMock).toHaveBeenCalledWith(archivePath, cachePath); expect(saveCacheMock).toHaveBeenCalledTimes(1); expect(saveCacheMock).toHaveBeenCalledWith(primaryKey, archivePath); diff --git a/__tests__/tar.test.ts b/__tests__/tar.test.ts new file mode 100644 index 0000000..55ff4c7 --- /dev/null +++ b/__tests__/tar.test.ts @@ -0,0 +1,58 @@ +import * as exec from "@actions/exec"; +import * as io from "@actions/io"; +import * as tar from "../src/tar"; + +jest.mock("@actions/exec"); +jest.mock("@actions/io"); + +beforeAll(() => { + jest.spyOn(io, "which").mockImplementation(tool => { + return Promise.resolve(tool); + }); +}); + +test("extract tar", async () => { + const mkdirMock = jest.spyOn(io, "mkdirP"); + const execMock = jest.spyOn(exec, "exec"); + + const archivePath = "cache.tar"; + const targetDirectory = "~/.npm/cache"; + await tar.extractTar(archivePath, targetDirectory); + + expect(mkdirMock).toHaveBeenCalledWith(targetDirectory); + + const IS_WINDOWS = process.platform === "win32"; + const tarPath = IS_WINDOWS + ? `${process.env["windir"]}\\System32\\tar.exe` + : "tar"; + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith(`"${tarPath}"`, [ + "-xz", + "-f", + archivePath, + "-C", + targetDirectory + ]); +}); + +test("create tar", async () => { + const execMock = jest.spyOn(exec, "exec"); + + const archivePath = "cache.tar"; + const sourceDirectory = "~/.npm/cache"; + await tar.createTar(archivePath, sourceDirectory); + + const IS_WINDOWS = process.platform === "win32"; + const tarPath = IS_WINDOWS + ? `${process.env["windir"]}\\System32\\tar.exe` + : "tar"; + expect(execMock).toHaveBeenCalledTimes(1); + expect(execMock).toHaveBeenCalledWith(`"${tarPath}"`, [ + "-cz", + "-f", + archivePath, + "-C", + sourceDirectory, + "." + ]); +}); diff --git a/package-lock.json b/package-lock.json index 2e8413e..986b08b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4859,9 +4859,9 @@ "dev": true }, "prettier": { - "version": "1.18.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", - "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", + "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", "dev": true }, "prettier-linter-helpers": { @@ -5983,9 +5983,9 @@ } }, "typescript": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", - "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz", + "integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw==", "dev": true }, "uglify-js": { diff --git a/package.json b/package.json index 42fbdbe..facc0af 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,8 @@ "jest": "^24.8.0", "jest-circus": "^24.7.1", "nock": "^11.7.0", - "prettier": "1.18.2", + "prettier": "^1.19.1", "ts-jest": "^24.0.2", - "typescript": "^3.6.4" + "typescript": "^3.7.3" } } diff --git a/src/restore.ts b/src/restore.ts index 15570cd..599dbd7 100644 --- a/src/restore.ts +++ b/src/restore.ts @@ -1,9 +1,8 @@ import * as core from "@actions/core"; -import { exec } from "@actions/exec"; -import * as io from "@actions/io"; import * as path from "path"; import * as cacheHttpClient from "./cacheHttpClient"; import { Events, Inputs, State } from "./constants"; +import { extractTar } from "./tar"; import * as utils from "./utils/actionUtils"; async function run(): Promise { @@ -87,27 +86,7 @@ async function run(): Promise { )} 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 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]; - - const tarPath = await io.which("tar", true); - core.debug(`Tar Path: ${tarPath}`); - - await exec(`"${tarPath}"`, args); + await extractTar(archivePath, cachePath); const isExactKeyMatch = utils.isExactKeyMatch( primaryKey, diff --git a/src/save.ts b/src/save.ts index 21f32d3..56198a7 100644 --- a/src/save.ts +++ b/src/save.ts @@ -1,9 +1,8 @@ import * as core from "@actions/core"; -import { exec } from "@actions/exec"; -import * as io from "@actions/io"; import * as path from "path"; import * as cacheHttpClient from "./cacheHttpClient"; import { Events, Inputs, State } from "./constants"; +import { createTar } from "./tar"; import * as utils from "./utils/actionUtils"; async function run(): Promise { @@ -46,24 +45,7 @@ async function run(): Promise { ); core.debug(`Archive Path: ${archivePath}`); - // http://man7.org/linux/man-pages/man1/tar.1.html - // tar [-options] [files or directories which to add into archive] - 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, "."]; - - const tarPath = await io.which("tar", true); - core.debug(`Tar Path: ${tarPath}`); - await exec(`"${tarPath}"`, args); + await createTar(archivePath, cachePath); const fileSizeLimit = 400 * 1024 * 1024; // 400MB const archiveFileSize = utils.getArchiveFileSize(archivePath); diff --git a/src/tar.ts b/src/tar.ts new file mode 100644 index 0000000..1f572d1 --- /dev/null +++ b/src/tar.ts @@ -0,0 +1,47 @@ +import { exec } from "@actions/exec"; +import * as io from "@actions/io"; +import { existsSync } from "fs"; + +async function getTarPath(): Promise { + // Explicitly use BSD Tar on Windows + const IS_WINDOWS = process.platform === "win32"; + if (IS_WINDOWS) { + const systemTar = `${process.env["windir"]}\\System32\\tar.exe`; + if (existsSync(systemTar)) { + return systemTar; + } + } + return await io.which("tar", true); +} + +async function execTar(args: string[]): Promise { + try { + await exec(`"${await getTarPath()}"`, args); + } catch (error) { + const IS_WINDOWS = process.platform === "win32"; + if (IS_WINDOWS) { + throw new Error( + `Tar failed with error: ${error?.message}. Ensure BSD tar is installed and on the PATH.` + ); + } + throw new Error(`Tar failed with error: ${error?.message}`); + } +} + +export async function extractTar( + archivePath: string, + targetDirectory: string +): Promise { + // Create directory to extract tar into + await io.mkdirP(targetDirectory); + const args = ["-xz", "-f", archivePath, "-C", targetDirectory]; + await execTar(args); +} + +export async function createTar( + archivePath: string, + sourceDirectory: string +): Promise { + const args = ["-cz", "-f", archivePath, "-C", sourceDirectory, "."]; + await execTar(args); +} From decbafc35082cddd7c1407de0e333c053d7ac180 Mon Sep 17 00:00:00 2001 From: Nogic <24802730+nogic1008@users.noreply.github.com> Date: Mon, 16 Dec 2019 09:45:29 +0900 Subject: [PATCH 33/37] Update examples.md Co-Authored-By: Chris Patterson --- examples.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples.md b/examples.md index 7552281..948179c 100644 --- a/examples.md +++ b/examples.md @@ -28,7 +28,7 @@ Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/packa Depending on the environment, huge packages might be pre-installed in the global cache folder. If you do not want to include them, consider to move the cache folder like below. ->Note: This workflow does not work for projects that require files to be placed in the global. +>Note: This workflow does not work for projects that require files to be placed in user profile package folder ```yaml env: NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages From a631fadf14ae5be452ecb86c94f71c2777361961 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Mon, 23 Dec 2019 07:30:34 -0800 Subject: [PATCH 34/37] README.md: fix grammar error (#136) "it's" is short for "it is," but the use in this sentence is as a possessive - something belonging to "it" - hence, "its" is correct. --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 70ae416..02cf6fe 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ on: push jobs: build: runs-on: ubuntu-latest - + steps: - uses: actions/checkout@v1 @@ -49,14 +49,14 @@ jobs: - name: Generate Prime Numbers if: steps.cache-primes.outputs.cache-hit != 'true' run: /generate-primes.sh -d prime-numbers - + - name: Use Prime Numbers run: /primes.sh -d prime-numbers ``` ## Implementation Examples -Every programming language and framework has it's own way of caching. +Every programming language and framework has its own way of caching. See [Examples](examples.md) for a list of `actions/cache` implementations for use with: @@ -93,7 +93,7 @@ steps: with: path: path/to/dependencies key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} - + - name: Install Dependencies if: steps.cache.outputs.cache-hit != 'true' run: /install.sh From b45d91cc4bac5c58ff24b19cd3b6711b2c5eca8f Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Mon, 6 Jan 2020 13:05:50 -0500 Subject: [PATCH 35/37] Chunked Cache Upload APIs (#128) * Initial pass at chunked upload apis * Fix cacheEntry type * Linting * Fix download cache entry tests * Linting tests * Pull in fixes from testing branch * Fix typo in ReserveCacheResponse * Add test convering reserve cache failure * Add retries to upload chunk * PR feedback * Format default chunk size * Remove responses array --- __tests__/restore.test.ts | 15 ++- __tests__/save.test.ts | 80 +++++++++++- package-lock.json | 2 +- package.json | 2 +- src/cacheHttpClient.ts | 250 +++++++++++++++++++++++++++++++------- src/contracts.d.ts | 13 ++ src/restore.ts | 7 +- src/save.ts | 16 ++- 8 files changed, 324 insertions(+), 61 deletions(-) diff --git a/__tests__/restore.test.ts b/__tests__/restore.test.ts index 15e0ba5..c96a2d6 100644 --- a/__tests__/restore.test.ts +++ b/__tests__/restore.test.ts @@ -248,7 +248,10 @@ test("restore with cache found", async () => { expect(getCacheMock).toHaveBeenCalledWith([key]); expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry); expect(createTempDirectoryMock).toHaveBeenCalledTimes(1); - expect(downloadCacheMock).toHaveBeenCalledWith(cacheEntry, archivePath); + expect(downloadCacheMock).toHaveBeenCalledWith( + cacheEntry.archiveLocation, + archivePath + ); expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath); expect(extractTarMock).toHaveBeenCalledTimes(1); @@ -312,7 +315,10 @@ test("restore with a pull request event and cache found", async () => { expect(getCacheMock).toHaveBeenCalledWith([key]); expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry); expect(createTempDirectoryMock).toHaveBeenCalledTimes(1); - expect(downloadCacheMock).toHaveBeenCalledWith(cacheEntry, archivePath); + expect(downloadCacheMock).toHaveBeenCalledWith( + cacheEntry.archiveLocation, + archivePath + ); expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath); expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~60 MB (62915000 B)`); @@ -377,7 +383,10 @@ test("restore with cache found for restore key", async () => { expect(getCacheMock).toHaveBeenCalledWith([key, restoreKey]); expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry); expect(createTempDirectoryMock).toHaveBeenCalledTimes(1); - expect(downloadCacheMock).toHaveBeenCalledWith(cacheEntry, archivePath); + expect(downloadCacheMock).toHaveBeenCalledWith( + cacheEntry.archiveLocation, + archivePath + ); expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath); expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~0 MB (142 B)`); diff --git a/__tests__/save.test.ts b/__tests__/save.test.ts index ba76cda..b355076 100644 --- a/__tests__/save.test.ts +++ b/__tests__/save.test.ts @@ -194,7 +194,7 @@ test("save with large cache outputs warning", async () => { const createTarMock = jest.spyOn(tar, "createTar"); - const cacheSize = 1024 * 1024 * 1024; //~1GB, over the 400MB limit + const cacheSize = 4 * 1024 * 1024 * 1024; //~4GB, over the 2GB limit jest.spyOn(actionUtils, "getArchiveFileSize").mockImplementationOnce(() => { return cacheSize; }); @@ -208,12 +208,63 @@ test("save with large cache outputs warning", async () => { expect(logWarningMock).toHaveBeenCalledTimes(1); expect(logWarningMock).toHaveBeenCalledWith( - "Cache size of ~1024 MB (1073741824 B) is over the 400MB limit, not saving cache." + "Cache size of ~4096 MB (4294967296 B) is over the 2GB limit, not saving cache." ); expect(failedMock).toHaveBeenCalledTimes(0); }); +test("save with reserve cache failure outputs warning", async () => { + const infoMock = jest.spyOn(core, "info"); + const logWarningMock = jest.spyOn(actionUtils, "logWarning"); + 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"; + testUtils.setInput(Inputs.Path, inputPath); + + const reserveCacheMock = jest + .spyOn(cacheHttpClient, "reserveCache") + .mockImplementationOnce(() => { + return Promise.resolve(-1); + }); + + const createTarMock = jest.spyOn(tar, "createTar"); + + const saveCacheMock = jest.spyOn(cacheHttpClient, "saveCache"); + + await run(); + + expect(reserveCacheMock).toHaveBeenCalledTimes(1); + expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey); + + expect(infoMock).toHaveBeenCalledWith( + `Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.` + ); + + expect(createTarMock).toHaveBeenCalledTimes(0); + expect(saveCacheMock).toHaveBeenCalledTimes(0); + expect(logWarningMock).toHaveBeenCalledTimes(0); + expect(failedMock).toHaveBeenCalledTimes(0); +}); + test("save with server error outputs warning", async () => { const logWarningMock = jest.spyOn(actionUtils, "logWarning"); const failedMock = jest.spyOn(core, "setFailed"); @@ -240,6 +291,13 @@ test("save with server error outputs warning", async () => { const cachePath = path.resolve(inputPath); testUtils.setInput(Inputs.Path, inputPath); + const cacheId = 4; + const reserveCacheMock = jest + .spyOn(cacheHttpClient, "reserveCache") + .mockImplementationOnce(() => { + return Promise.resolve(cacheId); + }); + const createTarMock = jest.spyOn(tar, "createTar"); const saveCacheMock = jest @@ -250,13 +308,16 @@ test("save with server error outputs warning", async () => { await run(); + expect(reserveCacheMock).toHaveBeenCalledTimes(1); + expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey); + const archivePath = path.join("/foo/bar", "cache.tgz"); expect(createTarMock).toHaveBeenCalledTimes(1); expect(createTarMock).toHaveBeenCalledWith(archivePath, cachePath); expect(saveCacheMock).toHaveBeenCalledTimes(1); - expect(saveCacheMock).toHaveBeenCalledWith(primaryKey, archivePath); + expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archivePath); expect(logWarningMock).toHaveBeenCalledTimes(1); expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred"); @@ -289,18 +350,29 @@ test("save with valid inputs uploads a cache", async () => { const cachePath = path.resolve(inputPath); testUtils.setInput(Inputs.Path, inputPath); + const cacheId = 4; + const reserveCacheMock = jest + .spyOn(cacheHttpClient, "reserveCache") + .mockImplementationOnce(() => { + return Promise.resolve(cacheId); + }); + const createTarMock = jest.spyOn(tar, "createTar"); + const saveCacheMock = jest.spyOn(cacheHttpClient, "saveCache"); await run(); + expect(reserveCacheMock).toHaveBeenCalledTimes(1); + expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey); + const archivePath = path.join("/foo/bar", "cache.tgz"); expect(createTarMock).toHaveBeenCalledTimes(1); expect(createTarMock).toHaveBeenCalledWith(archivePath, cachePath); expect(saveCacheMock).toHaveBeenCalledTimes(1); - expect(saveCacheMock).toHaveBeenCalledWith(primaryKey, archivePath); + expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archivePath); expect(failedMock).toHaveBeenCalledTimes(0); }); diff --git a/package-lock.json b/package-lock.json index 986b08b..37e50d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cache", - "version": "1.0.3", + "version": "1.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index facc0af..7de321b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cache", - "version": "1.0.3", + "version": "1.1.0", "private": true, "description": "Cache dependencies and build outputs", "main": "dist/restore/index.js", diff --git a/src/cacheHttpClient.ts b/src/cacheHttpClient.ts index 8a2014f..89bca63 100644 --- a/src/cacheHttpClient.ts +++ b/src/cacheHttpClient.ts @@ -1,26 +1,49 @@ 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 { HttpClient, HttpCodes } from "typed-rest-client/HttpClient"; import { IHttpClientResponse } from "typed-rest-client/Interfaces"; -import { IRequestOptions, RestClient } from "typed-rest-client/RestClient"; -import { ArtifactCacheEntry } from "./contracts"; +import { + IRequestOptions, + RestClient, + IRestResponse +} from "typed-rest-client/RestClient"; +import { + ArtifactCacheEntry, + CommitCacheRequest, + ReserveCacheRequest, + ReserveCacheResponse +} from "./contracts"; +import * as utils from "./utils/actionUtils"; -function getCacheUrl(): string { +function isSuccessStatusCode(statusCode: number): boolean { + return statusCode >= 200 && statusCode < 300; +} + +function isRetryableStatusCode(statusCode: number): boolean { + const retryableStatusCodes = [ + HttpCodes.BadGateway, + HttpCodes.ServiceUnavailable, + HttpCodes.GatewayTimeout + ]; + return retryableStatusCodes.includes(statusCode); +} + +function getCacheApiUrl(): string { // Ideally we just use ACTIONS_CACHE_URL - const cacheUrl: string = ( + const baseUrl: string = ( process.env["ACTIONS_CACHE_URL"] || process.env["ACTIONS_RUNTIME_URL"] || "" ).replace("pipelines", "artifactcache"); - if (!cacheUrl) { + if (!baseUrl) { throw new Error( "Cache Service Url not found, unable to restore cache." ); } - core.debug(`Cache Url: ${cacheUrl}`); - return cacheUrl; + core.debug(`Cache Url: ${baseUrl}`); + return `${baseUrl}_apis/artifactcache/`; } function createAcceptHeader(type: string, apiVersion: string): string { @@ -29,26 +52,26 @@ function createAcceptHeader(type: string, apiVersion: string): string { function getRequestOptions(): IRequestOptions { const requestOptions: IRequestOptions = { - acceptHeader: createAcceptHeader("application/json", "5.2-preview.1") + acceptHeader: createAcceptHeader("application/json", "6.0-preview.1") }; return requestOptions; } -export async function getCacheEntry( - keys: string[] -): Promise { - const cacheUrl = getCacheUrl(); +function createRestClient(): RestClient { const token = process.env["ACTIONS_RUNTIME_TOKEN"] || ""; const bearerCredentialHandler = new BearerCredentialHandler(token); - const resource = `_apis/artifactcache/cache?keys=${encodeURIComponent( - keys.join(",") - )}`; - - const restClient = new RestClient("actions/cache", cacheUrl, [ + return new RestClient("actions/cache", getCacheApiUrl(), [ bearerCredentialHandler ]); +} + +export async function getCacheEntry( + keys: string[] +): Promise { + const restClient = createRestClient(); + const resource = `cache?keys=${encodeURIComponent(keys.join(","))}`; const response = await restClient.get( resource, @@ -57,14 +80,15 @@ export async function getCacheEntry( if (response.statusCode === 204) { return null; } - if (response.statusCode !== 200) { + if (!isSuccessStatusCode(response.statusCode)) { throw new Error(`Cache service responded with ${response.statusCode}`); } const cacheResult = response.result; - if (!cacheResult || !cacheResult.archiveLocation) { + const cacheDownloadUrl = cacheResult?.archiveLocation; + if (!cacheDownloadUrl) { throw new Error("Cache not found."); } - core.setSecret(cacheResult.archiveLocation); + core.setSecret(cacheDownloadUrl); core.debug(`Cache Result:`); core.debug(JSON.stringify(cacheResult)); @@ -83,46 +107,178 @@ async function pipeResponseToStream( } export async function downloadCache( - cacheEntry: ArtifactCacheEntry, + archiveLocation: string, 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!); + const downloadResponse = await httpClient.get(archiveLocation); await pipeResponseToStream(downloadResponse, stream); } -export async function saveCache( - key: string, - archivePath: string +// Reserve Cache +export async function reserveCache(key: string): Promise { + const restClient = createRestClient(); + + const reserveCacheRequest: ReserveCacheRequest = { + key + }; + const response = await restClient.create( + "caches", + reserveCacheRequest, + getRequestOptions() + ); + + return response?.result?.cacheId ?? -1; +} + +function getContentRange(start: number, end: number): string { + // Format: `bytes start-end/filesize + // start and end are inclusive + // filesize can be * + // For a 200 byte chunk starting at byte 0: + // Content-Range: bytes 0-199/* + return `bytes ${start}-${end}/*`; +} + +async function uploadChunk( + restClient: RestClient, + resourceUrl: string, + data: NodeJS.ReadableStream, + start: number, + end: number ): Promise { - const stream = fs.createReadStream(archivePath); - - const cacheUrl = getCacheUrl(); - const token = process.env["ACTIONS_RUNTIME_TOKEN"] || ""; - const bearerCredentialHandler = new BearerCredentialHandler(token); - - const resource = `_apis/artifactcache/cache/${encodeURIComponent(key)}`; - const postUrl = cacheUrl + resource; - - const restClient = new RestClient("actions/cache", undefined, [ - bearerCredentialHandler - ]); - + core.debug( + `Uploading chunk of size ${end - + start + + 1} bytes at offset ${start} with content range: ${getContentRange( + start, + end + )}` + ); const requestOptions = getRequestOptions(); requestOptions.additionalHeaders = { - "Content-Type": "application/octet-stream" + "Content-Type": "application/octet-stream", + "Content-Range": getContentRange(start, end) }; - const response = await restClient.uploadStream( - "POST", - postUrl, - stream, + const uploadChunkRequest = async (): Promise> => { + return await restClient.uploadStream( + "PATCH", + resourceUrl, + data, + requestOptions + ); + }; + + const response = await uploadChunkRequest(); + if (isSuccessStatusCode(response.statusCode)) { + return; + } + + if (isRetryableStatusCode(response.statusCode)) { + core.debug( + `Received ${response.statusCode}, retrying chunk at offset ${start}.` + ); + const retryResponse = await uploadChunkRequest(); + if (isSuccessStatusCode(retryResponse.statusCode)) { + return; + } + } + + throw new Error( + `Cache service responded with ${response.statusCode} during chunk upload.` + ); +} + +async function uploadFile( + restClient: RestClient, + cacheId: number, + archivePath: string +): Promise { + // Upload Chunks + const fileSize = fs.statSync(archivePath).size; + const resourceUrl = getCacheApiUrl() + "caches/" + cacheId.toString(); + const fd = fs.openSync(archivePath, "r"); + + const concurrency = Number(process.env["CACHE_UPLOAD_CONCURRENCY"]) ?? 4; // # of HTTP requests in parallel + const MAX_CHUNK_SIZE = + Number(process.env["CACHE_UPLOAD_CHUNK_SIZE"]) ?? 32 * 1024 * 1024; // 32 MB Chunks + core.debug(`Concurrency: ${concurrency} and Chunk Size: ${MAX_CHUNK_SIZE}`); + + const parallelUploads = [...new Array(concurrency).keys()]; + core.debug("Awaiting all uploads"); + let offset = 0; + + try { + await Promise.all( + parallelUploads.map(async () => { + while (offset < fileSize) { + const chunkSize = Math.min( + fileSize - offset, + MAX_CHUNK_SIZE + ); + const start = offset; + const end = offset + chunkSize - 1; + offset += MAX_CHUNK_SIZE; + const chunk = fs.createReadStream(archivePath, { + fd, + start, + end, + autoClose: false + }); + + await uploadChunk( + restClient, + resourceUrl, + chunk, + start, + end + ); + } + }) + ); + } finally { + fs.closeSync(fd); + } + return; +} + +async function commitCache( + restClient: RestClient, + cacheId: number, + filesize: number +): Promise> { + const requestOptions = getRequestOptions(); + const commitCacheRequest: CommitCacheRequest = { size: filesize }; + return await restClient.create( + `caches/${cacheId.toString()}`, + commitCacheRequest, requestOptions ); - if (response.statusCode !== 200) { - throw new Error(`Cache service responded with ${response.statusCode}`); +} + +export async function saveCache( + cacheId: number, + archivePath: string +): Promise { + const restClient = createRestClient(); + + core.debug("Upload cache"); + await uploadFile(restClient, cacheId, archivePath); + + // Commit Cache + core.debug("Commiting cache"); + const cacheSize = utils.getArchiveFileSize(archivePath); + const commitCacheResponse = await commitCache( + restClient, + cacheId, + cacheSize + ); + if (!isSuccessStatusCode(commitCacheResponse.statusCode)) { + throw new Error( + `Cache service responded with ${commitCacheResponse.statusCode} during commit cache.` + ); } core.info("Cache saved successfully"); diff --git a/src/contracts.d.ts b/src/contracts.d.ts index 8478b83..269c7d9 100644 --- a/src/contracts.d.ts +++ b/src/contracts.d.ts @@ -4,3 +4,16 @@ export interface ArtifactCacheEntry { creationTime?: string; archiveLocation?: string; } + +export interface CommitCacheRequest { + size: number; +} + +export interface ReserveCacheRequest { + key: string; + version?: string; +} + +export interface ReserveCacheResponse { + cacheId: number; +} diff --git a/src/restore.ts b/src/restore.ts index 599dbd7..4911e7e 100644 --- a/src/restore.ts +++ b/src/restore.ts @@ -60,7 +60,7 @@ async function run(): Promise { try { const cacheEntry = await cacheHttpClient.getCacheEntry(keys); - if (!cacheEntry) { + if (!cacheEntry?.archiveLocation) { core.info( `Cache not found for input keys: ${keys.join(", ")}.` ); @@ -77,7 +77,10 @@ async function run(): Promise { utils.setCacheState(cacheEntry); // Download the cache from the cache entry - await cacheHttpClient.downloadCache(cacheEntry, archivePath); + await cacheHttpClient.downloadCache( + cacheEntry.archiveLocation, + archivePath + ); const archiveFileSize = utils.getArchiveFileSize(archivePath); core.info( diff --git a/src/save.ts b/src/save.ts index 56198a7..ee64e42 100644 --- a/src/save.ts +++ b/src/save.ts @@ -34,6 +34,15 @@ async function run(): Promise { return; } + core.debug("Reserving Cache"); + const cacheId = await cacheHttpClient.reserveCache(primaryKey); + if (cacheId == -1) { + core.info( + `Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.` + ); + return; + } + core.debug(`Cache ID: ${cacheId}`); const cachePath = utils.resolvePath( core.getInput(Inputs.Path, { required: true }) ); @@ -47,19 +56,20 @@ async function run(): Promise { await createTar(archivePath, cachePath); - const fileSizeLimit = 400 * 1024 * 1024; // 400MB + const fileSizeLimit = 2 * 1024 * 1024 * 1024; // 2GB per repo limit const archiveFileSize = utils.getArchiveFileSize(archivePath); core.debug(`File Size: ${archiveFileSize}`); if (archiveFileSize > fileSizeLimit) { utils.logWarning( `Cache size of ~${Math.round( archiveFileSize / (1024 * 1024) - )} MB (${archiveFileSize} B) is over the 400MB limit, not saving cache.` + )} MB (${archiveFileSize} B) is over the 2GB limit, not saving cache.` ); return; } - await cacheHttpClient.saveCache(primaryKey, archivePath); + core.debug(`Saving Cache (ID: ${cacheId})`); + await cacheHttpClient.saveCache(cacheId, archivePath); } catch (error) { utils.logWarning(error.message); } From 1da52de10fce90415c1b3f62692a935f5fd0f141 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Mon, 6 Jan 2020 13:31:03 -0500 Subject: [PATCH 36/37] npm audit fix --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 37e50d2..d60f8ae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2854,9 +2854,9 @@ "dev": true }, "handlebars": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.1.tgz", - "integrity": "sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -5989,9 +5989,9 @@ "dev": true }, "uglify-js": { - "version": "3.6.7", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.7.tgz", - "integrity": "sha512-4sXQDzmdnoXiO+xvmTzQsfIiwrjUCSA95rSP4SEd8tDb51W2TiDOlL76Hl+Kw0Ie42PSItCW8/t6pBNCF2R48A==", + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.3.tgz", + "integrity": "sha512-7tINm46/3puUA4hCkKYo4Xdts+JDaVC9ZPRcG8Xw9R4nhO/gZgUM3TENq8IF4Vatk8qCig4MzP/c8G4u2BkVQg==", "dev": true, "optional": true, "requires": { From c262ac0154d56cf1469a3fd6250544fb2fdd6c72 Mon Sep 17 00:00:00 2001 From: Josh Gross Date: Mon, 6 Jan 2020 14:06:24 -0500 Subject: [PATCH 37/37] Fix number parsing issues --- src/cacheHttpClient.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/cacheHttpClient.ts b/src/cacheHttpClient.ts index 89bca63..97c9672 100644 --- a/src/cacheHttpClient.ts +++ b/src/cacheHttpClient.ts @@ -191,6 +191,14 @@ async function uploadChunk( ); } +function parseEnvNumber(key: string): number | undefined { + const value = Number(process.env[key]); + if (Number.isNaN(value) || value < 0) { + return undefined; + } + return value; +} + async function uploadFile( restClient: RestClient, cacheId: number, @@ -201,9 +209,9 @@ async function uploadFile( const resourceUrl = getCacheApiUrl() + "caches/" + cacheId.toString(); const fd = fs.openSync(archivePath, "r"); - const concurrency = Number(process.env["CACHE_UPLOAD_CONCURRENCY"]) ?? 4; // # of HTTP requests in parallel + const concurrency = parseEnvNumber("CACHE_UPLOAD_CONCURRENCY") ?? 4; // # of HTTP requests in parallel const MAX_CHUNK_SIZE = - Number(process.env["CACHE_UPLOAD_CHUNK_SIZE"]) ?? 32 * 1024 * 1024; // 32 MB Chunks + parseEnvNumber("CACHE_UPLOAD_CHUNK_SIZE") ?? 32 * 1024 * 1024; // 32 MB Chunks core.debug(`Concurrency: ${concurrency} and Chunk Size: ${MAX_CHUNK_SIZE}`); const parallelUploads = [...new Array(concurrency).keys()];