Compare commits

..

2 Commits

Author SHA1 Message Date
0da99ad140 Preview v0.0.2 Release (#50)
* Create CODE_OF_CONDUCT.md

* Update workflow (#1)

* Run workflow on linux, mac, and windows

* Add status badge

* Use npm install instead

* Bump typescript version

* Use node 12.x

* Add Ruby Gem example (#4)

* Add Cocoapods example (#5)

* Add Carthage example (#10)

* Move examples to their own page (#13)

* Minor typo in README (#15)

from `steps.[ID].outupts.cache-hit` to `steps.[ID].outputs.cache-hit`

* Update README.md

* Prevent commands from executing during tests (#21)

* Prevent commands from executing during tests

* Add newline at end of file

* Drop all commands from output

* Link to NuGet lock files documentation (#20)

* Add trailing dash to Maven fallback key (#19)

* Fix README.md (#25)

`restore-keys` had incorrect indentation.

* Exclude documentation from CI tests (#28)

* Ignore all .md files

* Add note about time-based eviction to README (#30)

* Fix typo in error message (#29)

* Time based eviction interval is 1 week (#34)

* Remove cache checksum debug - close #24 (#26)

* Remove cache checksum debug - close #24

*  Remove cache checksum debug on save

* Fix formatting

* Add Go modules example (#18)

* Add Go modules example

* Fix TOC

* Fix repo name in contact email (#41)

* Add Elixir Mix example (#42)

* Add Elixir Mix example

* Fix typo

* Add cargo example for Rust project (#8)

* Add cargo example

* Add hash of Cargo.lock to keys of caches

* Move Rust example to examples.md

* Stop warning when cache is not found (#40)

The cache not being found is a common situation so very visible warning
is a little too much.

* Bump package version

* Release v0.0.2
2019-11-04 13:39:29 -05:00
e7ad80454a Release preview version 2019-10-30 14:50:22 -04:00
22 changed files with 431 additions and 2860 deletions

View File

@ -1,16 +0,0 @@
{
"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"]
}

View File

@ -1,11 +1,6 @@
name: Tests name: Tests
on: on:
pull_request: pull_request:
branches:
- master
paths-ignore:
- '**.md'
push: push:
branches: branches:
- master - master
@ -15,11 +10,9 @@ on:
jobs: jobs:
test: test:
name: Test on ${{ matrix.os }} name: Test on ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest, windows-latest, macOS-latest]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
@ -29,25 +22,10 @@ jobs:
with: with:
node-version: '12.x' 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 - run: npm ci
- name: Prettier Format Check - name: Prettier Format Check
run: npm run format-check run: npm run format-check
- name: ESLint Check
run: npm run lint
- name: Build & Test - name: Build & Test
run: npm run test run: npm run test

View File

@ -2,11 +2,7 @@
This GitHub Action allows caching dependencies and build outputs to improve workflow execution time. This GitHub Action allows caching dependencies and build outputs to improve workflow execution time.
<a href="https://github.com/actions/cache/actions?query=workflow%3ATests"><img alt="GitHub Actions status" src="https://github.com/actions/cache/workflows/Tests/badge.svg?branch=master&event=push"></a> <a href="https://github.com/actions/cache"><img alt="GitHub Actions status" src="https://github.com/actions/cache/workflows/Tests/badge.svg"></a>
## 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 ## Usage
@ -28,7 +24,7 @@ Create a workflow `.yml` file in your repositories `.github/workflows` directory
### Example workflow ### Example workflow
```yaml ```yaml
name: Caching Primes name: Example Caching with npm
on: push on: push
@ -39,19 +35,22 @@ jobs:
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Cache Primes - name: Cache node modules
id: cache-primes uses: actions/cache@preview
uses: actions/cache@v1
with: with:
path: prime-numbers path: node_modules
key: ${{ runner.os }}-primes key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Generate Prime Numbers - name: Install Dependencies
if: steps.cache-primes.outputs.cache-hit != 'true' run: npm install
run: /generate-primes.sh -d prime-numbers
- name: Use Prime Numbers - name: Build
run: /primes.sh -d prime-numbers run: npm run build
- name: Test
run: npm run test
``` ```
## Ecosystem Examples ## Ecosystem Examples
@ -60,7 +59,7 @@ See [Examples](examples.md)
## Cache Limits ## Cache Limits
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. 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.
## Skipping steps based on cache-hit ## Skipping steps based on cache-hit
@ -71,11 +70,11 @@ Example:
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- uses: actions/cache@v1 - uses: actions/cache@preview
id: cache id: cache
with: with:
path: path/to/dependencies path: path/to/dependencies
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }} key: ${{ runner.os }}-${{ hashFiles('**/lockfiles')}}
- name: Install Dependencies - name: Install Dependencies
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'

View File

@ -1 +0,0 @@
hello world

View File

@ -1,236 +0,0 @@
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("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;
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);
});

22
__tests__/main.test.ts Normal file
View File

@ -0,0 +1,22 @@
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"
);
});

View File

@ -1,394 +0,0 @@
import * as core from "@actions/core";
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("../src/cacheHttpClient");
jest.mock("../src/tar");
jest.mock("../src/utils/actionUtils");
beforeAll(() => {
jest.spyOn(actionUtils, "resolvePath").mockImplementation(filePath => {
return path.resolve(filePath);
});
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isExactKeyMatch(key, cacheResult);
}
);
jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.isValidEvent();
});
jest.spyOn(actionUtils, "getSupportedEvents").mockImplementation(() => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
return actualUtils.getSupportedEvents();
});
});
beforeEach(() => {
process.env[Events.Key] = Events.Push;
});
afterEach(() => {
testUtils.clearInputs();
delete process.env[Events.Key];
});
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(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 () => {
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 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(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 logWarningMock = jest.spyOn(actionUtils, "logWarning");
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(logWarningMock).toHaveBeenCalledTimes(1);
expect(logWarningMock).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 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(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 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 extractTarMock = jest.spyOn(tar, "extractTar");
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(extractTarMock).toHaveBeenCalledTimes(1);
expect(extractTarMock).toHaveBeenCalledWith(archivePath, cachePath);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith(true);
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
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 failedMock = jest.spyOn(core, "setFailed");
const stateMock = jest.spyOn(core, "saveState");
const cacheEntry: ArtifactCacheEntry = {
cacheKey: key,
scope: "refs/heads/master",
archiveLocation: "www.actionscache.test/download"
};
const getCacheMock = jest.spyOn(cacheHttpClient, "getCacheEntry");
getCacheMock.mockImplementation(() => {
return Promise.resolve(cacheEntry);
});
const tempPath = "/foo/bar";
const createTempDirectoryMock = jest.spyOn(
actionUtils,
"createTempDirectory"
);
createTempDirectoryMock.mockImplementation(() => {
return Promise.resolve(tempPath);
});
const archivePath = path.join(tempPath, "cache.tgz");
const setCacheStateMock = jest.spyOn(actionUtils, "setCacheState");
const downloadCacheMock = jest.spyOn(cacheHttpClient, "downloadCache");
const fileSize = 62915000;
const getArchiveFileSizeMock = jest
.spyOn(actionUtils, "getArchiveFileSize")
.mockReturnValue(fileSize);
const extractTarMock = jest.spyOn(tar, "extractTar");
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
await run();
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(getCacheMock).toHaveBeenCalledWith([key]);
expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry);
expect(createTempDirectoryMock).toHaveBeenCalledTimes(1);
expect(downloadCacheMock).toHaveBeenCalledWith(cacheEntry, archivePath);
expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath);
expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~60 MB (62915000 B)`);
expect(extractTarMock).toHaveBeenCalledTimes(1);
expect(extractTarMock).toHaveBeenCalledWith(archivePath, cachePath);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith(true);
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
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 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 extractTarMock = jest.spyOn(tar, "extractTar");
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
await run();
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
expect(getCacheMock).toHaveBeenCalledWith([key, restoreKey]);
expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry);
expect(createTempDirectoryMock).toHaveBeenCalledTimes(1);
expect(downloadCacheMock).toHaveBeenCalledWith(cacheEntry, archivePath);
expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath);
expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~0 MB (142 B)`);
expect(extractTarMock).toHaveBeenCalledTimes(1);
expect(extractTarMock).toHaveBeenCalledWith(archivePath, cachePath);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
expect(infoMock).toHaveBeenCalledWith(
`Cache restored from key: ${restoreKey}`
);
expect(failedMock).toHaveBeenCalledTimes(0);
});

View File

@ -1,306 +0,0 @@
import * as core from "@actions/core";
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("../src/cacheHttpClient");
jest.mock("../src/tar");
jest.mock("../src/utils/actionUtils");
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, "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);
});
jest.spyOn(actionUtils, "createTempDirectory").mockImplementation(() => {
return Promise.resolve("/foo/bar");
});
});
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 logWarningMock = jest.spyOn(actionUtils, "logWarning");
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(logWarningMock).toHaveBeenCalledWith(
`Error retrieving key from state.`
);
expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(failedMock).toHaveBeenCalledTimes(0);
});
test("save with exact match returns early", async () => {
const infoMock = jest.spyOn(core, "info");
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 createTarMock = jest.spyOn(tar, "createTar");
await run();
expect(infoMock).toHaveBeenCalledWith(
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
);
expect(createTarMock).toHaveBeenCalledTimes(0);
expect(failedMock).toHaveBeenCalledTimes(0);
});
test("save with missing input outputs warning", async () => {
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;
});
await run();
expect(logWarningMock).toHaveBeenCalledWith(
"Input required and not supplied: path"
);
expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(failedMock).toHaveBeenCalledTimes(0);
});
test("save with large cache outputs warning", async () => {
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";
const cachePath = path.resolve(inputPath);
testUtils.setInput(Inputs.Path, inputPath);
const createTarMock = jest.spyOn(tar, "createTar");
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");
expect(createTarMock).toHaveBeenCalledTimes(1);
expect(createTarMock).toHaveBeenCalledWith(archivePath, cachePath);
expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(logWarningMock).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 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";
const cachePath = path.resolve(inputPath);
testUtils.setInput(Inputs.Path, inputPath);
const createTarMock = jest.spyOn(tar, "createTar");
const saveCacheMock = jest
.spyOn(cacheHttpClient, "saveCache")
.mockImplementationOnce(() => {
throw new Error("HTTP Error Occurred");
});
await run();
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(logWarningMock).toHaveBeenCalledTimes(1);
expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
expect(failedMock).toHaveBeenCalledTimes(0);
});
test("save with valid inputs uploads a cache", async () => {
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 createTarMock = jest.spyOn(tar, "createTar");
const saveCacheMock = jest.spyOn(cacheHttpClient, "saveCache");
await run();
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(failedMock).toHaveBeenCalledTimes(0);
});

View File

@ -1,60 +0,0 @@
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(() => {
process.env["windir"] = "C:";
jest.spyOn(io, "which").mockImplementation(tool => {
return Promise.resolve(tool);
});
});
afterAll(() => {
delete process.env["windir"];
});
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 ? "C:\\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 ? "C:\\System32\\tar.exe" : "tar";
expect(execMock).toHaveBeenCalledTimes(1);
expect(execMock).toHaveBeenCalledWith(`"${tarPath}"`, [
"-cz",
"-f",
archivePath,
"-C",
sourceDirectory,
"."
]);
});

225
dist/restore/index.js vendored
View File

@ -1496,26 +1496,6 @@ const fs = __importStar(__webpack_require__(747));
const Handlers_1 = __webpack_require__(941); const Handlers_1 = __webpack_require__(941);
const HttpClient_1 = __webpack_require__(874); const HttpClient_1 = __webpack_require__(874);
const RestClient_1 = __webpack_require__(105); const RestClient_1 = __webpack_require__(105);
function getCacheUrl() {
// Ideally we just use ACTIONS_CACHE_URL
const cacheUrl = (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, apiVersion) {
return `${type};api-version=${apiVersion}`;
}
function getRequestOptions() {
const requestOptions = {
acceptHeader: createAcceptHeader("application/json", "5.2-preview.1")
};
return requestOptions;
}
function getCacheEntry(keys) { function getCacheEntry(keys) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const cacheUrl = getCacheUrl(); const cacheUrl = getCacheUrl();
@ -1533,16 +1513,24 @@ function getCacheEntry(keys) {
throw new Error(`Cache service responded with ${response.statusCode}`); throw new Error(`Cache service responded with ${response.statusCode}`);
} }
const cacheResult = response.result; const cacheResult = response.result;
core.debug(`Cache Result:`);
core.debug(JSON.stringify(cacheResult));
if (!cacheResult || !cacheResult.archiveLocation) { if (!cacheResult || !cacheResult.archiveLocation) {
throw new Error("Cache not found."); throw new Error("Cache not found.");
} }
core.setSecret(cacheResult.archiveLocation);
core.debug(`Cache Result:`);
core.debug(JSON.stringify(cacheResult));
return cacheResult; return cacheResult;
}); });
} }
exports.getCacheEntry = getCacheEntry; exports.getCacheEntry = getCacheEntry;
function downloadCache(cacheEntry, archivePath) {
return __awaiter(this, void 0, void 0, function* () {
const stream = fs.createWriteStream(archivePath);
const httpClient = new HttpClient_1.HttpClient("actions/cache");
const downloadResponse = yield httpClient.get(cacheEntry.archiveLocation);
yield pipeResponseToStream(downloadResponse, stream);
});
}
exports.downloadCache = downloadCache;
function pipeResponseToStream(response, stream) { function pipeResponseToStream(response, stream) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
return new Promise(resolve => { return new Promise(resolve => {
@ -1552,19 +1540,8 @@ function pipeResponseToStream(response, stream) {
}); });
}); });
} }
function downloadCache(cacheEntry, archivePath) { function saveCache(stream, key) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const stream = fs.createWriteStream(archivePath);
const httpClient = new HttpClient_1.HttpClient("actions/cache");
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const downloadResponse = yield httpClient.get(cacheEntry.archiveLocation);
yield pipeResponseToStream(downloadResponse, stream);
});
}
exports.downloadCache = downloadCache;
function saveCache(key, archivePath) {
return __awaiter(this, void 0, void 0, function* () {
const stream = fs.createReadStream(archivePath);
const cacheUrl = getCacheUrl(); const cacheUrl = getCacheUrl();
const token = process.env["ACTIONS_RUNTIME_TOKEN"] || ""; const token = process.env["ACTIONS_RUNTIME_TOKEN"] || "";
const bearerCredentialHandler = new Handlers_1.BearerCredentialHandler(token); const bearerCredentialHandler = new Handlers_1.BearerCredentialHandler(token);
@ -1585,6 +1562,26 @@ function saveCache(key, archivePath) {
}); });
} }
exports.saveCache = saveCache; exports.saveCache = saveCache;
function getRequestOptions() {
const requestOptions = {
acceptHeader: createAcceptHeader("application/json", "5.2-preview.1")
};
return requestOptions;
}
function createAcceptHeader(type, apiVersion) {
return `${type};api-version=${apiVersion}`;
}
function getCacheUrl() {
// Ideally we just use ACTIONS_CACHE_URL
let cacheUrl = (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;
}
/***/ }), /***/ }),
@ -2142,7 +2139,6 @@ var __importStar = (this && this.__importStar) || function (mod) {
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(__webpack_require__(470)); const core = __importStar(__webpack_require__(470));
const io = __importStar(__webpack_require__(1)); const io = __importStar(__webpack_require__(1));
const fs = __importStar(__webpack_require__(747));
const os = __importStar(__webpack_require__(87)); const os = __importStar(__webpack_require__(87));
const path = __importStar(__webpack_require__(622)); const path = __importStar(__webpack_require__(622));
const uuidV4 = __importStar(__webpack_require__(826)); const uuidV4 = __importStar(__webpack_require__(826));
@ -2174,10 +2170,6 @@ function createTempDirectory() {
}); });
} }
exports.createTempDirectory = createTempDirectory; exports.createTempDirectory = createTempDirectory;
function getArchiveFileSize(path) {
return fs.statSync(path).size;
}
exports.getArchiveFileSize = getArchiveFileSize;
function isExactKeyMatch(key, cacheResult) { function isExactKeyMatch(key, cacheResult) {
return !!(cacheResult && return !!(cacheResult &&
cacheResult.cacheKey && cacheResult.cacheKey &&
@ -2186,14 +2178,6 @@ function isExactKeyMatch(key, cacheResult) {
}) === 0); }) === 0);
} }
exports.isExactKeyMatch = isExactKeyMatch; exports.isExactKeyMatch = isExactKeyMatch;
function setCacheState(state) {
core.saveState(constants_1.State.CacheResult, JSON.stringify(state));
}
exports.setCacheState = setCacheState;
function setCacheHitOutput(isCacheHit) {
core.setOutput(constants_1.Outputs.CacheHit, isCacheHit.toString());
}
exports.setCacheHitOutput = setCacheHitOutput;
function setOutputAndState(key, cacheResult) { function setOutputAndState(key, cacheResult) {
setCacheHitOutput(isExactKeyMatch(key, cacheResult)); setCacheHitOutput(isExactKeyMatch(key, cacheResult));
// Store the cache result if it exists // Store the cache result if it exists
@ -2203,17 +2187,17 @@ exports.setOutputAndState = setOutputAndState;
function getCacheState() { function getCacheState() {
const stateData = core.getState(constants_1.State.CacheResult); const stateData = core.getState(constants_1.State.CacheResult);
core.debug(`State: ${stateData}`); core.debug(`State: ${stateData}`);
if (stateData) { return (stateData && JSON.parse(stateData));
return JSON.parse(stateData);
}
return undefined;
} }
exports.getCacheState = getCacheState; exports.getCacheState = getCacheState;
function logWarning(message) { function setCacheState(state) {
const warningPrefix = "[warning]"; core.saveState(constants_1.State.CacheResult, JSON.stringify(state));
core.info(`${warningPrefix}${message}`);
} }
exports.logWarning = logWarning; exports.setCacheState = setCacheState;
function setCacheHitOutput(isCacheHit) {
core.setOutput(constants_1.Outputs.CacheHit, isCacheHit.toString());
}
exports.setCacheHitOutput = setCacheHitOutput;
function resolvePath(filePath) { function resolvePath(filePath) {
if (filePath[0] === "~") { if (filePath[0] === "~") {
const home = os.homedir(); const home = os.homedir();
@ -2225,18 +2209,6 @@ function resolvePath(filePath) {
return path.resolve(filePath); return path.resolve(filePath);
} }
exports.resolvePath = resolvePath; exports.resolvePath = resolvePath;
function getSupportedEvents() {
return [constants_1.Events.Push, constants_1.Events.PullRequest];
}
exports.getSupportedEvents = getSupportedEvents;
// 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
function isValidEvent() {
const githubEvent = process.env[constants_1.Events.Key] || "";
return getSupportedEvents().includes(githubEvent);
}
exports.isValidEvent = isValidEvent;
/***/ }), /***/ }),
@ -2864,25 +2836,19 @@ function isUnixExecutable(stats) {
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
var Inputs; var Inputs;
(function (Inputs) { (function (Inputs) {
Inputs["Key"] = "key"; Inputs.Key = "key";
Inputs["Path"] = "path"; Inputs.Path = "path";
Inputs["RestoreKeys"] = "restore-keys"; Inputs.RestoreKeys = "restore-keys";
})(Inputs = exports.Inputs || (exports.Inputs = {})); })(Inputs = exports.Inputs || (exports.Inputs = {}));
var Outputs; var Outputs;
(function (Outputs) { (function (Outputs) {
Outputs["CacheHit"] = "cache-hit"; Outputs.CacheHit = "cache-hit";
})(Outputs = exports.Outputs || (exports.Outputs = {})); })(Outputs = exports.Outputs || (exports.Outputs = {}));
var State; var State;
(function (State) { (function (State) {
State["CacheKey"] = "CACHE_KEY"; State.CacheKey = "CACHE_KEY";
State["CacheResult"] = "CACHE_RESULT"; State.CacheResult = "CACHE_RESULT";
})(State = exports.State || (exports.State = {})); })(State = exports.State || (exports.State = {}));
var Events;
(function (Events) {
Events["Key"] = "GITHUB_EVENT_NAME";
Events["Push"] = "push";
Events["PullRequest"] = "pull_request";
})(Events = exports.Events || (exports.Events = {}));
/***/ }), /***/ }),
@ -2991,29 +2957,22 @@ var __importStar = (this && this.__importStar) || function (mod) {
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(__webpack_require__(470)); const core = __importStar(__webpack_require__(470));
const exec_1 = __webpack_require__(986);
const io = __importStar(__webpack_require__(1));
const fs = __importStar(__webpack_require__(747));
const path = __importStar(__webpack_require__(622)); const path = __importStar(__webpack_require__(622));
const cacheHttpClient = __importStar(__webpack_require__(154)); const cacheHttpClient = __importStar(__webpack_require__(154));
const constants_1 = __webpack_require__(694); const constants_1 = __webpack_require__(694);
const tar_1 = __webpack_require__(943);
const utils = __importStar(__webpack_require__(443)); const utils = __importStar(__webpack_require__(443));
function run() { function run() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
try { try {
// Validate inputs, this can cause task failure // Validate inputs, this can cause task failure
if (!utils.isValidEvent()) { let cachePath = utils.resolvePath(core.getInput(constants_1.Inputs.Path, { required: true }));
utils.logWarning(`Event Validation Error: The event type ${process.env[constants_1.Events.Key]} is not supported. Only ${utils
.getSupportedEvents()
.join(", ")} events are supported at this time.`);
return;
}
const cachePath = utils.resolvePath(core.getInput(constants_1.Inputs.Path, { required: true }));
core.debug(`Cache Path: ${cachePath}`); core.debug(`Cache Path: ${cachePath}`);
const primaryKey = core.getInput(constants_1.Inputs.Key, { required: true }); const primaryKey = core.getInput(constants_1.Inputs.Key, { required: true });
core.saveState(constants_1.State.CacheKey, primaryKey); core.saveState(constants_1.State.CacheKey, primaryKey);
const restoreKeys = core const restoreKeys = core.getInput(constants_1.Inputs.RestoreKeys).split("\n");
.getInput(constants_1.Inputs.RestoreKeys)
.split("\n")
.filter(x => x !== "");
const keys = [primaryKey, ...restoreKeys]; const keys = [primaryKey, ...restoreKeys];
core.debug("Resolved Keys:"); core.debug("Resolved Keys:");
core.debug(JSON.stringify(keys)); core.debug(JSON.stringify(keys));
@ -3035,24 +2994,37 @@ function run() {
try { try {
const cacheEntry = yield cacheHttpClient.getCacheEntry(keys); const cacheEntry = yield cacheHttpClient.getCacheEntry(keys);
if (!cacheEntry) { if (!cacheEntry) {
core.info(`Cache not found for input keys: ${keys.join(", ")}.`); core.info(`Cache not found for input keys: ${JSON.stringify(keys)}.`);
return; return;
} }
const archivePath = path.join(yield utils.createTempDirectory(), "cache.tgz"); let archivePath = path.join(yield utils.createTempDirectory(), "cache.tgz");
core.debug(`Archive Path: ${archivePath}`); core.debug(`Archive Path: ${archivePath}`);
// Store the cache result // Store the cache result
utils.setCacheState(cacheEntry); utils.setCacheState(cacheEntry);
// Download the cache from the cache entry // Download the cache from the cache entry
yield cacheHttpClient.downloadCache(cacheEntry, archivePath); yield cacheHttpClient.downloadCache(cacheEntry, archivePath);
const archiveFileSize = utils.getArchiveFileSize(archivePath); io.mkdirP(cachePath);
core.info(`Cache Size: ~${Math.round(archiveFileSize / (1024 * 1024))} MB (${archiveFileSize} B)`); // http://man7.org/linux/man-pages/man1/tar.1.html
yield tar_1.extractTar(archivePath, cachePath); // tar [-options] <name of the tar archive> [files or directories which to add into archive]
const args = ["-xz"];
const IS_WINDOWS = process.platform === "win32";
if (IS_WINDOWS) {
args.push("--force-local");
archivePath = archivePath.replace(/\\/g, "/");
cachePath = cachePath.replace(/\\/g, "/");
}
args.push(...["-f", archivePath, "-C", cachePath]);
const tarPath = yield io.which("tar", true);
core.debug(`Tar Path: ${tarPath}`);
const archiveFileSize = fs.statSync(archivePath).size;
core.debug(`File Size: ${archiveFileSize}`);
yield exec_1.exec(`"${tarPath}"`, args);
const isExactKeyMatch = utils.isExactKeyMatch(primaryKey, cacheEntry); const isExactKeyMatch = utils.isExactKeyMatch(primaryKey, cacheEntry);
utils.setCacheHitOutput(isExactKeyMatch); utils.setCacheHitOutput(isExactKeyMatch);
core.info(`Cache restored from key: ${cacheEntry && cacheEntry.cacheKey}`); core.info(`Cache restored from key: ${cacheEntry && cacheEntry.cacheKey}`);
} }
catch (error) { catch (error) {
utils.logWarning(error.message); core.warning(error.message);
utils.setCacheHitOutput(false); utils.setCacheHitOutput(false);
} }
} }
@ -5142,63 +5114,6 @@ var personalaccesstoken_1 = __webpack_require__(327);
exports.PersonalAccessTokenCredentialHandler = personalaccesstoken_1.PersonalAccessTokenCredentialHandler; exports.PersonalAccessTokenCredentialHandler = personalaccesstoken_1.PersonalAccessTokenCredentialHandler;
/***/ }),
/***/ 943:
/***/ (function(__unusedmodule, exports, __webpack_require__) {
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const exec_1 = __webpack_require__(986);
const io = __importStar(__webpack_require__(1));
function extractTar(archivePath, targetDirectory) {
return __awaiter(this, void 0, void 0, function* () {
// Create directory to extract tar into
yield io.mkdirP(targetDirectory);
// http://man7.org/linux/man-pages/man1/tar.1.html
// tar [-options] <name of the tar archive> [files or directories which to add into archive]
const args = ["-xz", "-f", archivePath, "-C", targetDirectory];
yield exec_1.exec(`"${yield getTarPath()}"`, args);
});
}
exports.extractTar = extractTar;
function createTar(archivePath, sourceDirectory) {
return __awaiter(this, void 0, void 0, function* () {
// http://man7.org/linux/man-pages/man1/tar.1.html
// tar [-options] <name of the tar archive> [files or directories which to add into archive]
const args = ["-cz", "-f", archivePath, "-C", sourceDirectory, "."];
yield exec_1.exec(`"${yield getTarPath()}"`, args);
});
}
exports.createTar = createTar;
function getTarPath() {
return __awaiter(this, void 0, void 0, function* () {
// Explicitly use BSD Tar on Windows
const IS_WINDOWS = process.platform === "win32";
return IS_WINDOWS
? `${process.env["windir"]}\\System32\\tar.exe`
: yield io.which("tar", true);
});
}
/***/ }), /***/ }),
/***/ 986: /***/ 986:

224
dist/save/index.js vendored
View File

@ -1496,26 +1496,6 @@ const fs = __importStar(__webpack_require__(747));
const Handlers_1 = __webpack_require__(941); const Handlers_1 = __webpack_require__(941);
const HttpClient_1 = __webpack_require__(874); const HttpClient_1 = __webpack_require__(874);
const RestClient_1 = __webpack_require__(105); const RestClient_1 = __webpack_require__(105);
function getCacheUrl() {
// Ideally we just use ACTIONS_CACHE_URL
const cacheUrl = (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, apiVersion) {
return `${type};api-version=${apiVersion}`;
}
function getRequestOptions() {
const requestOptions = {
acceptHeader: createAcceptHeader("application/json", "5.2-preview.1")
};
return requestOptions;
}
function getCacheEntry(keys) { function getCacheEntry(keys) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const cacheUrl = getCacheUrl(); const cacheUrl = getCacheUrl();
@ -1533,16 +1513,24 @@ function getCacheEntry(keys) {
throw new Error(`Cache service responded with ${response.statusCode}`); throw new Error(`Cache service responded with ${response.statusCode}`);
} }
const cacheResult = response.result; const cacheResult = response.result;
core.debug(`Cache Result:`);
core.debug(JSON.stringify(cacheResult));
if (!cacheResult || !cacheResult.archiveLocation) { if (!cacheResult || !cacheResult.archiveLocation) {
throw new Error("Cache not found."); throw new Error("Cache not found.");
} }
core.setSecret(cacheResult.archiveLocation);
core.debug(`Cache Result:`);
core.debug(JSON.stringify(cacheResult));
return cacheResult; return cacheResult;
}); });
} }
exports.getCacheEntry = getCacheEntry; exports.getCacheEntry = getCacheEntry;
function downloadCache(cacheEntry, archivePath) {
return __awaiter(this, void 0, void 0, function* () {
const stream = fs.createWriteStream(archivePath);
const httpClient = new HttpClient_1.HttpClient("actions/cache");
const downloadResponse = yield httpClient.get(cacheEntry.archiveLocation);
yield pipeResponseToStream(downloadResponse, stream);
});
}
exports.downloadCache = downloadCache;
function pipeResponseToStream(response, stream) { function pipeResponseToStream(response, stream) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
return new Promise(resolve => { return new Promise(resolve => {
@ -1552,19 +1540,8 @@ function pipeResponseToStream(response, stream) {
}); });
}); });
} }
function downloadCache(cacheEntry, archivePath) { function saveCache(stream, key) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
const stream = fs.createWriteStream(archivePath);
const httpClient = new HttpClient_1.HttpClient("actions/cache");
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const downloadResponse = yield httpClient.get(cacheEntry.archiveLocation);
yield pipeResponseToStream(downloadResponse, stream);
});
}
exports.downloadCache = downloadCache;
function saveCache(key, archivePath) {
return __awaiter(this, void 0, void 0, function* () {
const stream = fs.createReadStream(archivePath);
const cacheUrl = getCacheUrl(); const cacheUrl = getCacheUrl();
const token = process.env["ACTIONS_RUNTIME_TOKEN"] || ""; const token = process.env["ACTIONS_RUNTIME_TOKEN"] || "";
const bearerCredentialHandler = new Handlers_1.BearerCredentialHandler(token); const bearerCredentialHandler = new Handlers_1.BearerCredentialHandler(token);
@ -1585,6 +1562,26 @@ function saveCache(key, archivePath) {
}); });
} }
exports.saveCache = saveCache; exports.saveCache = saveCache;
function getRequestOptions() {
const requestOptions = {
acceptHeader: createAcceptHeader("application/json", "5.2-preview.1")
};
return requestOptions;
}
function createAcceptHeader(type, apiVersion) {
return `${type};api-version=${apiVersion}`;
}
function getCacheUrl() {
// Ideally we just use ACTIONS_CACHE_URL
let cacheUrl = (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;
}
/***/ }), /***/ }),
@ -2142,7 +2139,6 @@ var __importStar = (this && this.__importStar) || function (mod) {
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(__webpack_require__(470)); const core = __importStar(__webpack_require__(470));
const io = __importStar(__webpack_require__(1)); const io = __importStar(__webpack_require__(1));
const fs = __importStar(__webpack_require__(747));
const os = __importStar(__webpack_require__(87)); const os = __importStar(__webpack_require__(87));
const path = __importStar(__webpack_require__(622)); const path = __importStar(__webpack_require__(622));
const uuidV4 = __importStar(__webpack_require__(826)); const uuidV4 = __importStar(__webpack_require__(826));
@ -2174,10 +2170,6 @@ function createTempDirectory() {
}); });
} }
exports.createTempDirectory = createTempDirectory; exports.createTempDirectory = createTempDirectory;
function getArchiveFileSize(path) {
return fs.statSync(path).size;
}
exports.getArchiveFileSize = getArchiveFileSize;
function isExactKeyMatch(key, cacheResult) { function isExactKeyMatch(key, cacheResult) {
return !!(cacheResult && return !!(cacheResult &&
cacheResult.cacheKey && cacheResult.cacheKey &&
@ -2186,14 +2178,6 @@ function isExactKeyMatch(key, cacheResult) {
}) === 0); }) === 0);
} }
exports.isExactKeyMatch = isExactKeyMatch; exports.isExactKeyMatch = isExactKeyMatch;
function setCacheState(state) {
core.saveState(constants_1.State.CacheResult, JSON.stringify(state));
}
exports.setCacheState = setCacheState;
function setCacheHitOutput(isCacheHit) {
core.setOutput(constants_1.Outputs.CacheHit, isCacheHit.toString());
}
exports.setCacheHitOutput = setCacheHitOutput;
function setOutputAndState(key, cacheResult) { function setOutputAndState(key, cacheResult) {
setCacheHitOutput(isExactKeyMatch(key, cacheResult)); setCacheHitOutput(isExactKeyMatch(key, cacheResult));
// Store the cache result if it exists // Store the cache result if it exists
@ -2203,17 +2187,17 @@ exports.setOutputAndState = setOutputAndState;
function getCacheState() { function getCacheState() {
const stateData = core.getState(constants_1.State.CacheResult); const stateData = core.getState(constants_1.State.CacheResult);
core.debug(`State: ${stateData}`); core.debug(`State: ${stateData}`);
if (stateData) { return (stateData && JSON.parse(stateData));
return JSON.parse(stateData);
}
return undefined;
} }
exports.getCacheState = getCacheState; exports.getCacheState = getCacheState;
function logWarning(message) { function setCacheState(state) {
const warningPrefix = "[warning]"; core.saveState(constants_1.State.CacheResult, JSON.stringify(state));
core.info(`${warningPrefix}${message}`);
} }
exports.logWarning = logWarning; exports.setCacheState = setCacheState;
function setCacheHitOutput(isCacheHit) {
core.setOutput(constants_1.Outputs.CacheHit, isCacheHit.toString());
}
exports.setCacheHitOutput = setCacheHitOutput;
function resolvePath(filePath) { function resolvePath(filePath) {
if (filePath[0] === "~") { if (filePath[0] === "~") {
const home = os.homedir(); const home = os.homedir();
@ -2225,18 +2209,6 @@ function resolvePath(filePath) {
return path.resolve(filePath); return path.resolve(filePath);
} }
exports.resolvePath = resolvePath; exports.resolvePath = resolvePath;
function getSupportedEvents() {
return [constants_1.Events.Push, constants_1.Events.PullRequest];
}
exports.getSupportedEvents = getSupportedEvents;
// 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
function isValidEvent() {
const githubEvent = process.env[constants_1.Events.Key] || "";
return getSupportedEvents().includes(githubEvent);
}
exports.isValidEvent = isValidEvent;
/***/ }), /***/ }),
@ -2879,47 +2851,56 @@ var __importStar = (this && this.__importStar) || function (mod) {
}; };
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(__webpack_require__(470)); const core = __importStar(__webpack_require__(470));
const exec_1 = __webpack_require__(986);
const io = __importStar(__webpack_require__(1));
const fs = __importStar(__webpack_require__(747));
const path = __importStar(__webpack_require__(622)); const path = __importStar(__webpack_require__(622));
const cacheHttpClient = __importStar(__webpack_require__(154)); const cacheHttpClient = __importStar(__webpack_require__(154));
const constants_1 = __webpack_require__(694); const constants_1 = __webpack_require__(694);
const tar_1 = __webpack_require__(943);
const utils = __importStar(__webpack_require__(443)); const utils = __importStar(__webpack_require__(443));
function run() { function run() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
try { try {
if (!utils.isValidEvent()) {
utils.logWarning(`Event Validation Error: The event type ${process.env[constants_1.Events.Key]} is not supported. Only ${utils
.getSupportedEvents()
.join(", ")} events are supported at this time.`);
return;
}
const state = utils.getCacheState(); const state = utils.getCacheState();
// Inputs are re-evaluted before the post action, so we want the original key used for restore // Inputs are re-evaluted before the post action, so we want the original key used for restore
const primaryKey = core.getState(constants_1.State.CacheKey); const primaryKey = core.getState(constants_1.State.CacheKey);
if (!primaryKey) { if (!primaryKey) {
utils.logWarning(`Error retrieving key from state.`); core.warning(`Error retrieving key from state.`);
return; return;
} }
if (utils.isExactKeyMatch(primaryKey, state)) { if (utils.isExactKeyMatch(primaryKey, state)) {
core.info(`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`); core.info(`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`);
return; return;
} }
const cachePath = utils.resolvePath(core.getInput(constants_1.Inputs.Path, { required: true })); let cachePath = utils.resolvePath(core.getInput(constants_1.Inputs.Path, { required: true }));
core.debug(`Cache Path: ${cachePath}`); core.debug(`Cache Path: ${cachePath}`);
const archivePath = path.join(yield utils.createTempDirectory(), "cache.tgz"); let archivePath = path.join(yield utils.createTempDirectory(), "cache.tgz");
core.debug(`Archive Path: ${archivePath}`); core.debug(`Archive Path: ${archivePath}`);
yield tar_1.createTar(archivePath, cachePath); // http://man7.org/linux/man-pages/man1/tar.1.html
const fileSizeLimit = 400 * 1024 * 1024; // 400MB // tar [-options] <name of the tar archive> [files or directories which to add into archive]
const archiveFileSize = utils.getArchiveFileSize(archivePath); 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 tarPath = yield io.which("tar", true);
core.debug(`Tar Path: ${tarPath}`);
yield exec_1.exec(`"${tarPath}"`, args);
const fileSizeLimit = 200 * 1024 * 1024; // 200MB
const archiveFileSize = fs.statSync(archivePath).size;
core.debug(`File Size: ${archiveFileSize}`); core.debug(`File Size: ${archiveFileSize}`);
if (archiveFileSize > fileSizeLimit) { if (archiveFileSize > fileSizeLimit) {
utils.logWarning(`Cache size of ~${Math.round(archiveFileSize / (1024 * 1024))} MB (${archiveFileSize} B) is over the 400MB limit, not saving cache.`); core.warning(`Cache size of ${archiveFileSize} bytes is over the 200MB limit, not saving cache.`);
return; return;
} }
yield cacheHttpClient.saveCache(primaryKey, archivePath); const stream = fs.createReadStream(archivePath);
yield cacheHttpClient.saveCache(stream, primaryKey);
} }
catch (error) { catch (error) {
utils.logWarning(error.message); core.warning(error.message);
} }
}); });
} }
@ -2937,25 +2918,19 @@ exports.default = run;
Object.defineProperty(exports, "__esModule", { value: true }); Object.defineProperty(exports, "__esModule", { value: true });
var Inputs; var Inputs;
(function (Inputs) { (function (Inputs) {
Inputs["Key"] = "key"; Inputs.Key = "key";
Inputs["Path"] = "path"; Inputs.Path = "path";
Inputs["RestoreKeys"] = "restore-keys"; Inputs.RestoreKeys = "restore-keys";
})(Inputs = exports.Inputs || (exports.Inputs = {})); })(Inputs = exports.Inputs || (exports.Inputs = {}));
var Outputs; var Outputs;
(function (Outputs) { (function (Outputs) {
Outputs["CacheHit"] = "cache-hit"; Outputs.CacheHit = "cache-hit";
})(Outputs = exports.Outputs || (exports.Outputs = {})); })(Outputs = exports.Outputs || (exports.Outputs = {}));
var State; var State;
(function (State) { (function (State) {
State["CacheKey"] = "CACHE_KEY"; State.CacheKey = "CACHE_KEY";
State["CacheResult"] = "CACHE_RESULT"; State.CacheResult = "CACHE_RESULT";
})(State = exports.State || (exports.State = {})); })(State = exports.State || (exports.State = {}));
var Events;
(function (Events) {
Events["Key"] = "GITHUB_EVENT_NAME";
Events["Push"] = "push";
Events["PullRequest"] = "pull_request";
})(Events = exports.Events || (exports.Events = {}));
/***/ }), /***/ }),
@ -5116,63 +5091,6 @@ var personalaccesstoken_1 = __webpack_require__(327);
exports.PersonalAccessTokenCredentialHandler = personalaccesstoken_1.PersonalAccessTokenCredentialHandler; exports.PersonalAccessTokenCredentialHandler = personalaccesstoken_1.PersonalAccessTokenCredentialHandler;
/***/ }),
/***/ 943:
/***/ (function(__unusedmodule, exports, __webpack_require__) {
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const exec_1 = __webpack_require__(986);
const io = __importStar(__webpack_require__(1));
function extractTar(archivePath, targetDirectory) {
return __awaiter(this, void 0, void 0, function* () {
// Create directory to extract tar into
yield io.mkdirP(targetDirectory);
// http://man7.org/linux/man-pages/man1/tar.1.html
// tar [-options] <name of the tar archive> [files or directories which to add into archive]
const args = ["-xz", "-f", archivePath, "-C", targetDirectory];
yield exec_1.exec(`"${yield getTarPath()}"`, args);
});
}
exports.extractTar = extractTar;
function createTar(archivePath, sourceDirectory) {
return __awaiter(this, void 0, void 0, function* () {
// http://man7.org/linux/man-pages/man1/tar.1.html
// tar [-options] <name of the tar archive> [files or directories which to add into archive]
const args = ["-cz", "-f", archivePath, "-C", sourceDirectory, "."];
yield exec_1.exec(`"${yield getTarPath()}"`, args);
});
}
exports.createTar = createTar;
function getTarPath() {
return __awaiter(this, void 0, void 0, function* () {
// Explicitly use BSD Tar on Windows
const IS_WINDOWS = process.platform === "win32";
return IS_WINDOWS
? `${process.env["windir"]}\\System32\\tar.exe`
: yield io.which("tar", true);
});
}
/***/ }), /***/ }),
/***/ 986: /***/ 986:

View File

@ -1,24 +1,43 @@
# Examples # Examples
- [C# - Nuget](#c---nuget)
- [Elixir - Mix](#elixir---mix)
- [Go - Modules](#go---modules)
- [Java - Gradle](#java---gradle)
- [Java - Maven](#java---maven)
- [Node - npm](#node---npm) - [Node - npm](#node---npm)
- [Node - Yarn](#node---yarn) - [Node - Yarn](#node---yarn)
- [PHP - Composer](#php---composer) - [C# - Nuget](#c---nuget)
- [Python - pip](#python---pip) - [Java - Gradle](#java---gradle)
- [Ruby - Gem](#ruby---gem) - [Java - Maven](#java---maven)
- [Rust - Cargo](#rust---cargo)
- [Swift, Objective-C - Carthage](#swift-objective-c---carthage) - [Swift, Objective-C - Carthage](#swift-objective-c---carthage)
- [Swift, Objective-C - CocoaPods](#swift-objective-c---cocoapods) - [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@preview
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
```
## Node - Yarn
```yaml
- uses: actions/cache@preview
with:
path: ~/.cache/yarn
key: ${{ runner.os }}-yarn-${{ hashFiles(format('{0}{1}', github.workspace, '/yarn.lock')) }}
restore-keys: |
${{ runner.os }}-yarn-
```
## C# - Nuget ## C# - Nuget
Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/package-references-in-project-files#locking-dependencies): Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/package-references-in-project-files#locking-dependencies):
```yaml ```yaml
- uses: actions/cache@v1 - uses: actions/cache@preview
with: with:
path: ~/.nuget/packages path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
@ -26,31 +45,10 @@ Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/packa
${{ runner.os }}-nuget- ${{ 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 ## Java - Gradle
```yaml ```yaml
- uses: actions/cache@v1 - uses: actions/cache@preview
with: with:
path: ~/.gradle/caches path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
@ -61,7 +59,7 @@ Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/packa
## Java - Maven ## Java - Maven
```yaml ```yaml
- uses: actions/cache@v1 - uses: actions/cache@preview
with: with:
path: ~/.m2/repository path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@ -69,203 +67,76 @@ Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/packa
${{ runner.os }}-maven- ${{ runner.os }}-maven-
``` ```
## 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: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
```
### Windows
```yaml
- uses: actions/cache@v1
with:
path: ~\AppData\Roaming\npm-cache
key: ${{ runner.os }}-node-${{ hashFiles('**\package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
```
### Using multiple systems and `npm config`
```yaml
- name: Get npm cache directory
id: npm-cache
run: |
echo "::set-output name=dir::$(npm config get cache)"
- uses: actions/cache@v1
with:
path: ${{ steps.npm-cache.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
```
## Node - Yarn
The yarn cache directory will depend on your operating system and version of `yarn`. See https://yarnpkg.com/lang/en/docs/cli/cache/ for more info.
```yaml
- name: Get yarn cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
```
## PHP - Composer
```yaml
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- uses: actions/cache@v1
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
```
## Python - pip
For pip, the cache directory will vary by OS. See https://pip.pypa.io/en/stable/reference/pip_install/#caching
Locations:
- Ubuntu: `~/.cache/pip`
- Windows: `~\AppData\Local\pip\Cache`
- macOS: `~/Library/Caches/pip`
### Simple example
```yaml
- uses: actions/cache@v1
with:
path: ~/.cache/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
- uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
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
```yaml
- name: Cache cargo registry
uses: actions/cache@v1
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v1
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
```
## Swift, Objective-C - Carthage ## Swift, Objective-C - Carthage
```yaml ```yaml
- uses: actions/cache@v1 uses: actions/cache@preview
with: with:
path: Carthage path: Carthage
key: ${{ runner.os }}-carthage-${{ hashFiles('**/Cartfile.resolved') }} key: ${{ runner.os }}-carthage-${{ hashFiles('**/Cartfile.resolved') }}
restore-keys: | restore-keys: |
${{ runner.os }}-carthage- ${{ runner.os }}-carthage-
``` ```
## Swift, Objective-C - CocoaPods ## Swift, Objective-C - CocoaPods
```yaml ```yaml
- uses: actions/cache@v1 - uses: actions/cache@preview
with: with:
path: Pods path: Pods
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }} key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
restore-keys: | restore-keys: |
${{ runner.os }}-pods- ${{ runner.os }}-pods-
``` ```
## Ruby - Gem
```yaml
- uses: actions/cache@preview
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
```
## Go - Modules
```yaml
- uses: actions/cache@preview
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
```
## Elixir - Mix
```yaml
- uses: actions/cache@preview
with:
path: deps
key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
restore-keys: |
${{ runner.os }}-mix-
```
## Rust - Cargo
```
- name: Cache cargo registry
uses: actions/cache@preview
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@preview
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@preview
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
```

View File

@ -1,23 +1,20 @@
require("nock").disableNetConnect();
module.exports = { module.exports = {
clearMocks: true, clearMocks: true,
moduleFileExtensions: ["js", "ts"], moduleFileExtensions: ['js', 'ts'],
testEnvironment: "node", testEnvironment: 'node',
testMatch: ["**/*.test.ts"], testMatch: ['**/*.test.ts'],
testRunner: "jest-circus/runner", testRunner: 'jest-circus/runner',
transform: { transform: {
"^.+\\.ts$": "ts-jest" '^.+\\.ts$': 'ts-jest'
}, },
verbose: true 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) => { process.stdout.write = (str, encoding, cb) => {
// Core library will directly call process.stdout.write for commands // Core library will directly call process.stdout.write for commands
// We don't want :: commands to be executed by the runner during tests // We don't want :: commands to be executed by the runner during tests
if (!str.match(/^::/)) { if (!str.match(/^::/)) {
return processStdoutWrite(str, encoding, cb); return processStdoutWrite(str, encoding, cb);
} }
}; }

1045
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,12 @@
{ {
"name": "cache", "name": "cache",
"version": "1.0.3", "version": "0.0.2",
"private": true, "private": true,
"description": "Cache dependencies and build outputs", "description": "Cache dependencies and build outputs",
"main": "dist/restore/index.js", "main": "dist/restore/index.js",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"test": "tsc --noEmit && jest --coverage", "test": "tsc --noEmit && jest --coverage",
"lint": "eslint **/*.ts --cache",
"format": "prettier --write **/*.ts", "format": "prettier --write **/*.ts",
"format-check": "prettier --check **/*.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/" "release": "ncc build -o dist/restore src/restore.ts && ncc build -o dist/save src/save.ts && git add -f dist/"
@ -32,20 +31,11 @@
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^24.0.13", "@types/jest": "^24.0.13",
"@types/nock": "^11.1.0",
"@types/node": "^12.0.4", "@types/node": "^12.0.4",
"@types/uuid": "^3.4.5", "@types/uuid": "^3.4.5",
"@typescript-eslint/eslint-plugin": "^2.7.0",
"@typescript-eslint/parser": "^2.7.0",
"@zeit/ncc": "^0.20.5", "@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": "^24.8.0",
"jest-circus": "^24.7.1", "jest-circus": "^24.7.1",
"nock": "^11.7.0",
"prettier": "1.18.2", "prettier": "1.18.2",
"ts-jest": "^24.0.2", "ts-jest": "^24.0.2",
"typescript": "^3.6.4" "typescript": "^3.6.4"

View File

@ -1,40 +1,13 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
import * as fs from "fs"; import * as fs from "fs";
import { BearerCredentialHandler } from "typed-rest-client/Handlers"; import { BearerCredentialHandler } from "typed-rest-client/Handlers";
import { HttpClient } from "typed-rest-client/HttpClient"; import { HttpClient } from "typed-rest-client/HttpClient";
import { IHttpClientResponse } from "typed-rest-client/Interfaces"; import { IHttpClientResponse } from "typed-rest-client/Interfaces";
import { IRequestOptions, RestClient } from "typed-rest-client/RestClient"; import { RestClient, IRequestOptions } from "typed-rest-client/RestClient";
import { ArtifactCacheEntry } from "./contracts"; 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( export async function getCacheEntry(
keys: string[] keys: string[]
): Promise<ArtifactCacheEntry | null> { ): Promise<ArtifactCacheEntry | null> {
@ -61,16 +34,25 @@ export async function getCacheEntry(
throw new Error(`Cache service responded with ${response.statusCode}`); throw new Error(`Cache service responded with ${response.statusCode}`);
} }
const cacheResult = response.result; const cacheResult = response.result;
core.debug(`Cache Result:`);
core.debug(JSON.stringify(cacheResult));
if (!cacheResult || !cacheResult.archiveLocation) { if (!cacheResult || !cacheResult.archiveLocation) {
throw new Error("Cache not found."); throw new Error("Cache not found.");
} }
core.setSecret(cacheResult.archiveLocation);
core.debug(`Cache Result:`);
core.debug(JSON.stringify(cacheResult));
return cacheResult; return cacheResult;
} }
export async function downloadCache(
cacheEntry: ArtifactCacheEntry,
archivePath: string
): Promise<void> {
const stream = fs.createWriteStream(archivePath);
const httpClient = new HttpClient("actions/cache");
const downloadResponse = await httpClient.get(cacheEntry.archiveLocation!);
await pipeResponseToStream(downloadResponse, stream);
}
async function pipeResponseToStream( async function pipeResponseToStream(
response: IHttpClientResponse, response: IHttpClientResponse,
stream: NodeJS.WritableStream stream: NodeJS.WritableStream
@ -82,23 +64,7 @@ async function pipeResponseToStream(
}); });
} }
export async function downloadCache( export async function saveCache(stream: NodeJS.ReadableStream, key: string) {
cacheEntry: ArtifactCacheEntry,
archivePath: string
): Promise<void> {
const stream = fs.createWriteStream(archivePath);
const httpClient = new HttpClient("actions/cache");
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const downloadResponse = await httpClient.get(cacheEntry.archiveLocation!);
await pipeResponseToStream(downloadResponse, stream);
}
export async function saveCache(
key: string,
archivePath: string
): Promise<void> {
const stream = fs.createReadStream(archivePath);
const cacheUrl = getCacheUrl(); const cacheUrl = getCacheUrl();
const token = process.env["ACTIONS_RUNTIME_TOKEN"] || ""; const token = process.env["ACTIONS_RUNTIME_TOKEN"] || "";
const bearerCredentialHandler = new BearerCredentialHandler(token); const bearerCredentialHandler = new BearerCredentialHandler(token);
@ -127,3 +93,32 @@ export async function saveCache(
core.info("Cache saved successfully"); 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;
}

View File

@ -1,20 +1,14 @@
export enum Inputs { export namespace Inputs {
Key = "key", export const Key = "key";
Path = "path", export const Path = "path";
RestoreKeys = "restore-keys" export const RestoreKeys = "restore-keys";
} }
export enum Outputs { export namespace Outputs {
CacheHit = "cache-hit" export const CacheHit = "cache-hit";
} }
export enum State { export namespace State {
CacheKey = "CACHE_KEY", export const CacheKey = "CACHE_KEY";
CacheResult = "CACHE_RESULT" export const CacheResult = "CACHE_RESULT";
}
export enum Events {
Key = "GITHUB_EVENT_NAME",
Push = "push",
PullRequest = "pull_request"
} }

View File

@ -1,25 +1,18 @@
import * as core from "@actions/core"; 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 path from "path";
import * as cacheHttpClient from "./cacheHttpClient"; import * as cacheHttpClient from "./cacheHttpClient";
import { Events, Inputs, State } from "./constants"; import { Inputs, State } from "./constants";
import { extractTar } from "./tar";
import * as utils from "./utils/actionUtils"; import * as utils from "./utils/actionUtils";
async function run(): Promise<void> { async function run() {
try { try {
// Validate inputs, this can cause task failure // Validate inputs, this can cause task failure
if (!utils.isValidEvent()) { let cachePath = utils.resolvePath(
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(
core.getInput(Inputs.Path, { required: true }) core.getInput(Inputs.Path, { required: true })
); );
core.debug(`Cache Path: ${cachePath}`); core.debug(`Cache Path: ${cachePath}`);
@ -27,10 +20,7 @@ async function run(): Promise<void> {
const primaryKey = core.getInput(Inputs.Key, { required: true }); const primaryKey = core.getInput(Inputs.Key, { required: true });
core.saveState(State.CacheKey, primaryKey); core.saveState(State.CacheKey, primaryKey);
const restoreKeys = core const restoreKeys = core.getInput(Inputs.RestoreKeys).split("\n");
.getInput(Inputs.RestoreKeys)
.split("\n")
.filter(x => x !== "");
const keys = [primaryKey, ...restoreKeys]; const keys = [primaryKey, ...restoreKeys];
core.debug("Resolved Keys:"); core.debug("Resolved Keys:");
@ -62,12 +52,12 @@ async function run(): Promise<void> {
const cacheEntry = await cacheHttpClient.getCacheEntry(keys); const cacheEntry = await cacheHttpClient.getCacheEntry(keys);
if (!cacheEntry) { if (!cacheEntry) {
core.info( core.info(
`Cache not found for input keys: ${keys.join(", ")}.` `Cache not found for input keys: ${JSON.stringify(keys)}.`
); );
return; return;
} }
const archivePath = path.join( let archivePath = path.join(
await utils.createTempDirectory(), await utils.createTempDirectory(),
"cache.tgz" "cache.tgz"
); );
@ -79,14 +69,27 @@ async function run(): Promise<void> {
// Download the cache from the cache entry // Download the cache from the cache entry
await cacheHttpClient.downloadCache(cacheEntry, archivePath); await cacheHttpClient.downloadCache(cacheEntry, archivePath);
const archiveFileSize = utils.getArchiveFileSize(archivePath); io.mkdirP(cachePath);
core.info(
`Cache Size: ~${Math.round(
archiveFileSize / (1024 * 1024)
)} MB (${archiveFileSize} B)`
);
await extractTar(archivePath, cachePath); // http://man7.org/linux/man-pages/man1/tar.1.html
// tar [-options] <name of the tar archive> [files or directories which to add into archive]
const args = ["-xz"];
const IS_WINDOWS = process.platform === "win32";
if (IS_WINDOWS) {
args.push("--force-local");
archivePath = archivePath.replace(/\\/g, "/");
cachePath = cachePath.replace(/\\/g, "/");
}
args.push(...["-f", archivePath, "-C", cachePath]);
const 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( const isExactKeyMatch = utils.isExactKeyMatch(
primaryKey, primaryKey,
@ -98,7 +101,7 @@ async function run(): Promise<void> {
`Cache restored from key: ${cacheEntry && cacheEntry.cacheKey}` `Cache restored from key: ${cacheEntry && cacheEntry.cacheKey}`
); );
} catch (error) { } catch (error) {
utils.logWarning(error.message); core.warning(error.message);
utils.setCacheHitOutput(false); utils.setCacheHitOutput(false);
} }
} catch (error) { } catch (error) {

View File

@ -1,29 +1,22 @@
import * as core from "@actions/core"; 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 path from "path";
import * as cacheHttpClient from "./cacheHttpClient"; import * as cacheHttpClient from "./cacheHttpClient";
import { Events, Inputs, State } from "./constants"; import { Inputs, State } from "./constants";
import { createTar } from "./tar";
import * as utils from "./utils/actionUtils"; import * as utils from "./utils/actionUtils";
async function run(): Promise<void> { async function run() {
try { 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(); const state = utils.getCacheState();
// Inputs are re-evaluted before the post action, so we want the original key used for restore // Inputs are re-evaluted before the post action, so we want the original key used for restore
const primaryKey = core.getState(State.CacheKey); const primaryKey = core.getState(State.CacheKey);
if (!primaryKey) { if (!primaryKey) {
utils.logWarning(`Error retrieving key from state.`); core.warning(`Error retrieving key from state.`);
return; return;
} }
@ -34,34 +27,47 @@ async function run(): Promise<void> {
return; return;
} }
const cachePath = utils.resolvePath( let cachePath = utils.resolvePath(
core.getInput(Inputs.Path, { required: true }) core.getInput(Inputs.Path, { required: true })
); );
core.debug(`Cache Path: ${cachePath}`); core.debug(`Cache Path: ${cachePath}`);
const archivePath = path.join( let archivePath = path.join(
await utils.createTempDirectory(), await utils.createTempDirectory(),
"cache.tgz" "cache.tgz"
); );
core.debug(`Archive Path: ${archivePath}`); core.debug(`Archive Path: ${archivePath}`);
await createTar(archivePath, cachePath); // http://man7.org/linux/man-pages/man1/tar.1.html
// tar [-options] <name of the tar archive> [files or directories which to add into archive]
const args = ["-cz"];
const IS_WINDOWS = process.platform === "win32";
if (IS_WINDOWS) {
args.push("--force-local");
archivePath = archivePath.replace(/\\/g, "/");
cachePath = cachePath.replace(/\\/g, "/");
}
const fileSizeLimit = 400 * 1024 * 1024; // 400MB args.push(...["-f", archivePath, "-C", cachePath, "."]);
const archiveFileSize = utils.getArchiveFileSize(archivePath);
const tarPath = await io.which("tar", true);
core.debug(`Tar Path: ${tarPath}`);
await exec(`"${tarPath}"`, args);
const fileSizeLimit = 200 * 1024 * 1024; // 200MB
const archiveFileSize = fs.statSync(archivePath).size;
core.debug(`File Size: ${archiveFileSize}`); core.debug(`File Size: ${archiveFileSize}`);
if (archiveFileSize > fileSizeLimit) { if (archiveFileSize > fileSizeLimit) {
utils.logWarning( core.warning(
`Cache size of ~${Math.round( `Cache size of ${archiveFileSize} bytes is over the 200MB limit, not saving cache.`
archiveFileSize / (1024 * 1024)
)} MB (${archiveFileSize} B) is over the 400MB limit, not saving cache.`
); );
return; return;
} }
await cacheHttpClient.saveCache(primaryKey, archivePath); const stream = fs.createReadStream(archivePath);
await cacheHttpClient.saveCache(stream, primaryKey);
} catch (error) { } catch (error) {
utils.logWarning(error.message); core.warning(error.message);
} }
} }

View File

@ -1,33 +0,0 @@
import { exec } from "@actions/exec";
import * as io from "@actions/io";
async function getTarPath(): Promise<string> {
// Explicitly use BSD Tar on Windows
const IS_WINDOWS = process.platform === "win32";
return IS_WINDOWS
? `${process.env["windir"]}\\System32\\tar.exe`
: await io.which("tar", true);
}
export async function extractTar(
archivePath: string,
targetDirectory: string
): Promise<void> {
// Create directory to extract tar into
await io.mkdirP(targetDirectory);
// http://man7.org/linux/man-pages/man1/tar.1.html
// tar [-options] <name of the tar archive> [files or directories which to add into archive]
const args = ["-xz", "-f", archivePath, "-C", targetDirectory];
await exec(`"${await getTarPath()}"`, args);
}
export async function createTar(
archivePath: string,
sourceDirectory: string
): Promise<void> {
// http://man7.org/linux/man-pages/man1/tar.1.html
// tar [-options] <name of the tar archive> [files or directories which to add into archive]
const args = ["-cz", "-f", archivePath, "-C", sourceDirectory, "."];
await exec(`"${await getTarPath()}"`, args);
}

View File

@ -1,11 +1,10 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
import * as io from "@actions/io"; import * as io from "@actions/io";
import * as fs from "fs";
import * as os from "os"; import * as os from "os";
import * as path from "path"; import * as path from "path";
import * as uuidV4 from "uuid/v4"; import * as uuidV4 from "uuid/v4";
import { Events, Outputs, State } from "../constants"; import { Outputs, State } from "../constants";
import { ArtifactCacheEntry } from "../contracts"; import { ArtifactCacheEntry } from "../contracts";
// From https://github.com/actions/toolkit/blob/master/packages/tool-cache/src/tool-cache.ts#L23 // From https://github.com/actions/toolkit/blob/master/packages/tool-cache/src/tool-cache.ts#L23
@ -33,10 +32,6 @@ export async function createTempDirectory(): Promise<string> {
return dest; return dest;
} }
export function getArchiveFileSize(path: string): number {
return fs.statSync(path).size;
}
export function isExactKeyMatch( export function isExactKeyMatch(
key: string, key: string,
cacheResult?: ArtifactCacheEntry cacheResult?: ArtifactCacheEntry
@ -50,18 +45,10 @@ 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( export function setOutputAndState(
key: string, key: string,
cacheResult?: ArtifactCacheEntry cacheResult?: ArtifactCacheEntry
): void { ) {
setCacheHitOutput(isExactKeyMatch(key, cacheResult)); setCacheHitOutput(isExactKeyMatch(key, cacheResult));
// Store the cache result if it exists // Store the cache result if it exists
cacheResult && setCacheState(cacheResult); cacheResult && setCacheState(cacheResult);
@ -70,16 +57,15 @@ export function setOutputAndState(
export function getCacheState(): ArtifactCacheEntry | undefined { export function getCacheState(): ArtifactCacheEntry | undefined {
const stateData = core.getState(State.CacheResult); const stateData = core.getState(State.CacheResult);
core.debug(`State: ${stateData}`); core.debug(`State: ${stateData}`);
if (stateData) { return (stateData && JSON.parse(stateData)) as ArtifactCacheEntry;
return JSON.parse(stateData) as ArtifactCacheEntry;
}
return undefined;
} }
export function logWarning(message: string): void { export function setCacheState(state: ArtifactCacheEntry) {
const warningPrefix = "[warning]"; core.saveState(State.CacheResult, JSON.stringify(state));
core.info(`${warningPrefix}${message}`); }
export function setCacheHitOutput(isCacheHit: boolean) {
core.setOutput(Outputs.CacheHit, isCacheHit.toString());
} }
export function resolvePath(filePath: string): string { export function resolvePath(filePath: string): string {
@ -93,15 +79,3 @@ export function resolvePath(filePath: string): string {
return path.resolve(filePath); 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);
}

View File

@ -1,29 +1,7 @@
import { Inputs } from "../constants";
// See: https://github.com/actions/toolkit/blob/master/packages/core/src/core.ts#L67
function getInputName(name: string): string { function getInputName(name: string): string {
return `INPUT_${name.replace(/ /g, "_").toUpperCase()}`; return `INPUT_${name.replace(/ /g, "_").toUpperCase()}`;
} }
export function setInput(name: string, value: string): void { export function setInput(name: string, value: string) {
process.env[getInputName(name)] = value; process.env[getInputName(name)] = value;
} }
interface CacheInput {
path: string;
key: string;
restoreKeys?: string[];
}
export function setInputs(input: CacheInput): void {
setInput(Inputs.Path, input.path);
setInput(Inputs.Key, input.key);
input.restoreKeys &&
setInput(Inputs.RestoreKeys, input.restoreKeys.join("\n"));
}
export function clearInputs(): void {
delete process.env[getInputName(Inputs.Path)];
delete process.env[getInputName(Inputs.Key)];
delete process.env[getInputName(Inputs.RestoreKeys)];
}