mirror of
https://github.com/actions/cache.git
synced 2025-06-24 19:31:10 +02:00
Compare commits
93 Commits
Author | SHA1 | Date | |
---|---|---|---|
5ca27f25cb | |||
244fc0ba29 | |||
943d2a4dac | |||
01f7f296b1 | |||
ab48bf6f67 | |||
59a8d125e7 | |||
d29c1df198 | |||
d59a1464f2 | |||
481a91ba10 | |||
4978dc4f31 | |||
66891cb075 | |||
5c77066753 | |||
24fb121989 | |||
55bbcc2eab | |||
c5475843b3 | |||
0065ae9713 | |||
4aa79d91d3 | |||
5474af707f | |||
591646a21e | |||
eed9cfe64d | |||
b773382817 | |||
984ce638f0 | |||
ff937cc950 | |||
d60d2bef10 | |||
e561127c3e | |||
b8204782bb | |||
e6c708b5ce | |||
581312be20 | |||
9ab95382c8 | |||
6c7d57dc97 | |||
2b83e91661 | |||
1034aaeec8 | |||
bcc23b930f | |||
249a22026d | |||
7f9517a009 | |||
16a133d9a7 | |||
46fead7f5e | |||
bac1a40c81 | |||
916cc60b3c | |||
4967c8e6c5 | |||
a0024e2bd0 | |||
5ddc028cc8 | |||
05b13411a0 | |||
e756b19f93 | |||
354f70a56c | |||
ddc4681e8d | |||
29b4783cc7 | |||
2403bbedac | |||
ccc66f769e | |||
5d8c995f20 | |||
ce9276c90e | |||
9eb452c280 | |||
75cd46ec0c | |||
a5d9a3b1a6 | |||
97f7baa910 | |||
9ceee97d99 | |||
ccf9619480 | |||
9f07ee13de | |||
1ed0c23029 | |||
54626c4a4f | |||
48b62c1c52 | |||
9bb13c71ec | |||
8b2a57849f | |||
f00dedfa6c | |||
12b87469d4 | |||
52046d1409 | |||
08438313d5 | |||
7ccdf5c70d | |||
306f72536b | |||
4fa017f2b7 | |||
78809b91d7 | |||
a4e3c3b64e | |||
e5370355e6 | |||
0e86d5c038 | |||
2ba9edf492 | |||
f15bc7a0d9 | |||
b6b8aa78d8 | |||
272268544c | |||
64f8769515 | |||
4a724707e9 | |||
f60097cd16 | |||
eb78578266 | |||
22d71e33ad | |||
b13df3fa54 | |||
cae64ca3cd | |||
af8651e0c5 | |||
6c471ae9f6 | |||
206172ea8e | |||
5833d5c131 | |||
826785142a | |||
8e9c167fd7 | |||
e8230b28a9 | |||
4944275b95 |
@ -12,5 +12,12 @@
|
||||
"plugin:prettier/recommended",
|
||||
"prettier/@typescript-eslint"
|
||||
],
|
||||
"plugins": ["@typescript-eslint", "jest"]
|
||||
"plugins": ["@typescript-eslint", "simple-import-sort", "jest"],
|
||||
"rules": {
|
||||
"import/first": "error",
|
||||
"import/newline-after-import": "error",
|
||||
"import/no-duplicates": "error",
|
||||
"simple-import-sort/sort": "error",
|
||||
"sort-imports": "off"
|
||||
}
|
||||
}
|
||||
|
52
.github/workflows/codeql.yml
vendored
Normal file
52
.github/workflows/codeql.yml
vendored
Normal file
@ -0,0 +1,52 @@
|
||||
name: "Code scanning - action"
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
schedule:
|
||||
- cron: '0 19 * * 0'
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
|
||||
# CodeQL runs on ubuntu-latest and windows-latest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
fetch-depth: 2
|
||||
|
||||
# If this run was triggered by a pull request event, then checkout
|
||||
# the head of the pull request instead of the merge commit.
|
||||
- run: git checkout HEAD^2
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v1
|
||||
# Override language selection by uncommenting this and choosing your languages
|
||||
# with:
|
||||
# languages: go, javascript, csharp, python, cpp, java
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v1
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
#- run: |
|
||||
# make bootstrap
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v1
|
138
.github/workflows/workflow.yml
vendored
138
.github/workflows/workflow.yml
vendored
@ -3,52 +3,152 @@ name: Tests
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
- releases/**
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
- releases/**
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Test on ${{ matrix.os }}
|
||||
|
||||
# Build and unit test
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
os: [ubuntu-latest, ubuntu-16.04, windows-latest, macOS-latest]
|
||||
fail-fast: false
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- uses: actions/setup-node@v1
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: '12.x'
|
||||
|
||||
- name: Get npm cache directory
|
||||
- name: Determine npm cache directory
|
||||
id: npm-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(npm config get cache)"
|
||||
|
||||
- uses: actions/cache@v1
|
||||
- name: Restore npm cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.npm-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: npm ci
|
||||
|
||||
- name: Prettier Format Check
|
||||
run: npm run format-check
|
||||
|
||||
- name: ESLint Check
|
||||
run: npm run lint
|
||||
|
||||
- name: Build & Test
|
||||
run: npm run test
|
||||
- name: Ensure dist/ folder is up-to-date
|
||||
if: ${{ runner.os == 'Linux' }}
|
||||
shell: bash
|
||||
run: |
|
||||
npm run build
|
||||
if [ "$(git diff --ignore-space-at-eol | wc -l)" -gt "0" ]; then
|
||||
echo "Detected uncommitted changes after build. See status below:"
|
||||
git diff
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# End to end save and restore
|
||||
test-save:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, ubuntu-16.04, windows-latest, macOS-latest]
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Generate files in working directory
|
||||
shell: bash
|
||||
run: __tests__/create-cache-files.sh ${{ runner.os }} test-cache
|
||||
- name: Generate files outside working directory
|
||||
shell: bash
|
||||
run: __tests__/create-cache-files.sh ${{ runner.os }} ~/test-cache
|
||||
- name: Save cache
|
||||
uses: ./
|
||||
with:
|
||||
key: test-${{ runner.os }}-${{ github.run_id }}
|
||||
path: |
|
||||
test-cache
|
||||
~/test-cache
|
||||
test-restore:
|
||||
needs: test-save
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, ubuntu-16.04, windows-latest, macOS-latest]
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Restore cache
|
||||
uses: ./
|
||||
with:
|
||||
key: test-${{ runner.os }}-${{ github.run_id }}
|
||||
path: |
|
||||
test-cache
|
||||
~/test-cache
|
||||
- name: Verify cache files in working directory
|
||||
shell: bash
|
||||
run: __tests__/verify-cache-files.sh ${{ runner.os }} test-cache
|
||||
- name: Verify cache files outside working directory
|
||||
shell: bash
|
||||
run: __tests__/verify-cache-files.sh ${{ runner.os }} ~/test-cache
|
||||
|
||||
# End to end with proxy
|
||||
test-proxy-save:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ubuntu:latest
|
||||
options: --dns 127.0.0.1
|
||||
services:
|
||||
squid-proxy:
|
||||
image: datadog/squid:latest
|
||||
ports:
|
||||
- 3128:3128
|
||||
env:
|
||||
https_proxy: http://squid-proxy:3128
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Generate files
|
||||
run: __tests__/create-cache-files.sh proxy test-cache
|
||||
- name: Save cache
|
||||
uses: ./
|
||||
with:
|
||||
key: test-proxy-${{ github.run_id }}
|
||||
path: test-cache
|
||||
test-proxy-restore:
|
||||
needs: test-proxy-save
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ubuntu:latest
|
||||
options: --dns 127.0.0.1
|
||||
services:
|
||||
squid-proxy:
|
||||
image: datadog/squid:latest
|
||||
ports:
|
||||
- 3128:3128
|
||||
env:
|
||||
https_proxy: http://squid-proxy:3128
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Restore cache
|
||||
uses: ./
|
||||
with:
|
||||
key: test-proxy-${{ github.run_id }}
|
||||
path: test-cache
|
||||
- name: Verify cache
|
||||
run: __tests__/verify-cache-files.sh proxy test-cache
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,8 +1,5 @@
|
||||
__tests__/runner/*
|
||||
|
||||
# comment out in distribution branches
|
||||
dist/
|
||||
|
||||
node_modules/
|
||||
lib/
|
||||
|
||||
|
77
README.md
77
README.md
@ -2,12 +2,34 @@
|
||||
|
||||
This 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/actions?query=workflow%3ATests"><img alt="GitHub Actions status" src="https://github.com/actions/cache/workflows/Tests/badge.svg?branch=main&event=push"></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).
|
||||
|
||||
## What's New
|
||||
|
||||
* Added support for multiple paths, [glob patterns](https://github.com/actions/toolkit/tree/main/packages/glob), and single file caches.
|
||||
|
||||
```yaml
|
||||
- name: Cache multiple paths
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/cache
|
||||
!~/cache/exclude
|
||||
**/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
|
||||
```
|
||||
|
||||
* Increased performance and improved cache sizes using `zstd` compression for Linux and macOS runners
|
||||
* Allowed caching for all events with a ref. See [events that trigger workflow](https://help.github.com/en/actions/reference/events-that-trigger-workflows) for info on which events do not have a `GITHUB_REF`
|
||||
* Released the [`@actions/cache`](https://github.com/actions/toolkit/tree/main/packages/cache) npm package to allow other actions to utilize caching
|
||||
* Added a best-effort cleanup step to delete the archive after extraction to reduce storage space
|
||||
|
||||
Refer [here](https://github.com/actions/cache/blob/v1/README.md) for previous versions
|
||||
|
||||
## Usage
|
||||
|
||||
### Pre-requisites
|
||||
@ -15,7 +37,7 @@ Create a workflow `.yml` file in your repositories `.github/workflows` directory
|
||||
|
||||
### Inputs
|
||||
|
||||
* `path` - A directory to store and save the cache
|
||||
* `path` - A list of files, directories, and wildcard patterns to cache and restore. See [`@actions/glob`](https://github.com/actions/toolkit/tree/main/packages/glob) for supported patterns.
|
||||
* `key` - An explicit key for restoring and saving the cache
|
||||
* `restore-keys` - An ordered list of keys to use for restoring the cache if no cache hit occurred for key
|
||||
|
||||
@ -25,6 +47,11 @@ Create a workflow `.yml` file in your repositories `.github/workflows` directory
|
||||
|
||||
> See [Skipping steps based on cache-hit](#Skipping-steps-based-on-cache-hit) for info on using this output
|
||||
|
||||
### Cache scopes
|
||||
The cache is scoped to the key and branch. The default branch cache is available to other branches.
|
||||
|
||||
See [Matching a cache key](https://help.github.com/en/actions/configuring-and-managing-workflows/caching-dependencies-to-speed-up-workflows#matching-a-cache-key) for more info.
|
||||
|
||||
### Example workflow
|
||||
|
||||
```yaml
|
||||
@ -37,11 +64,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Cache Primes
|
||||
id: cache-primes
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: prime-numbers
|
||||
key: ${{ runner.os }}-primes
|
||||
@ -61,13 +88,16 @@ Every programming language and framework has its own way of caching.
|
||||
See [Examples](examples.md) for a list of `actions/cache` implementations for use with:
|
||||
|
||||
- [C# - Nuget](./examples.md#c---nuget)
|
||||
- [D - DUB](./examples.md#d---dub)
|
||||
- [Elixir - Mix](./examples.md#elixir---mix)
|
||||
- [Go - Modules](./examples.md#go---modules)
|
||||
- [Haskell - Cabal](./examples.md#haskell---cabal)
|
||||
- [Java - Gradle](./examples.md#java---gradle)
|
||||
- [Java - Maven](./examples.md#java---maven)
|
||||
- [Node - npm](./examples.md#node---npm)
|
||||
- [Node - Lerna](./examples.md#node---lerna)
|
||||
- [Node - Yarn](./examples.md#node---yarn)
|
||||
- [OCaml/Reason - esy](./examples.md#ocamlreason---esy)
|
||||
- [PHP - Composer](./examples.md#php---composer)
|
||||
- [Python - pip](./examples.md#python---pip)
|
||||
- [R - renv](./examples.md#r---renv)
|
||||
@ -78,6 +108,39 @@ See [Examples](examples.md) for a list of `actions/cache` implementations for us
|
||||
- [Swift, Objective-C - CocoaPods](./examples.md#swift-objective-c---cocoapods)
|
||||
- [Swift - Swift Package Manager](./examples.md#swift---swift-package-manager)
|
||||
|
||||
## Creating a cache key
|
||||
|
||||
A cache key can include any of the contexts, functions, literals, and operators supported by GitHub Actions.
|
||||
|
||||
For example, using the [`hashFiles`](https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#hashfiles) function allows you to create a new cache when dependencies change.
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
path/to/dependencies
|
||||
some/other/dependencies
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
|
||||
```
|
||||
|
||||
Additionally, you can use arbitrary command output in a cache key, such as a date or software version:
|
||||
|
||||
```yaml
|
||||
# http://man7.org/linux/man-pages/man1/date.1.html
|
||||
- name: Get Date
|
||||
id: get-date
|
||||
run: |
|
||||
echo "::set-output name=date::$(/bin/date -u "+%Y%m%d")"
|
||||
shell: bash
|
||||
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: path/to/dependencies
|
||||
key: ${{ runner.os }}-${{ steps.get-date.outputs.date }}-${{ hashFiles('**/lockfiles') }}
|
||||
```
|
||||
|
||||
See [Using contexts to create cache keys](https://help.github.com/en/actions/configuring-and-managing-workflows/caching-dependencies-to-speed-up-workflows#using-contexts-to-create-cache-keys)
|
||||
|
||||
## Cache Limits
|
||||
|
||||
A repository can have up to 5GB of caches. Once the 5GB 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.
|
||||
@ -89,9 +152,9 @@ Using the `cache-hit` output, subsequent steps (such as install or build) can be
|
||||
Example:
|
||||
```yaml
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
id: cache
|
||||
with:
|
||||
path: path/to/dependencies
|
||||
@ -105,7 +168,7 @@ steps:
|
||||
> Note: The `id` defined in `actions/cache` must match the `id` in the `if` statement (i.e. `steps.[ID].outputs.cache-hit`)
|
||||
|
||||
## Contributing
|
||||
We would love for you to contribute to `@actions/cache`, pull requests are welcome! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) for more information.
|
||||
We would love for you to contribute to `actions/cache`, pull requests are welcome! Please see the [CONTRIBUTING.md](CONTRIBUTING.md) for more information.
|
||||
|
||||
## License
|
||||
The scripts and documentation in this project are released under the [MIT License](LICENSE)
|
||||
|
@ -1,84 +1,72 @@
|
||||
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 { Events, Outputs, RefKey, State } from "../src/constants";
|
||||
import * as actionUtils from "../src/utils/actionUtils";
|
||||
import * as testUtils from "../src/utils/testUtils";
|
||||
|
||||
jest.mock("@actions/core");
|
||||
jest.mock("os");
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
|
||||
return jest.requireActual("@actions/core").getInput(name, options);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete process.env[Events.Key];
|
||||
delete process.env[RefKey];
|
||||
});
|
||||
|
||||
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", () => {
|
||||
test("isExactKeyMatch with undefined cache key returns false", () => {
|
||||
const key = "linux-rust";
|
||||
const cacheEntry = undefined;
|
||||
const cacheKey = undefined;
|
||||
|
||||
expect(actionUtils.isExactKeyMatch(key, cacheEntry)).toBe(false);
|
||||
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
|
||||
});
|
||||
|
||||
test("isExactKeyMatch with empty cache entry returns false", () => {
|
||||
test("isExactKeyMatch with empty cache key returns false", () => {
|
||||
const key = "linux-rust";
|
||||
const cacheEntry: ArtifactCacheEntry = {};
|
||||
const cacheKey = "";
|
||||
|
||||
expect(actionUtils.isExactKeyMatch(key, cacheEntry)).toBe(false);
|
||||
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
|
||||
});
|
||||
|
||||
test("isExactKeyMatch with different keys returns false", () => {
|
||||
const key = "linux-rust";
|
||||
const cacheEntry: ArtifactCacheEntry = {
|
||||
cacheKey: "linux-"
|
||||
};
|
||||
const cacheKey = "linux-";
|
||||
|
||||
expect(actionUtils.isExactKeyMatch(key, cacheEntry)).toBe(false);
|
||||
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
|
||||
});
|
||||
|
||||
test("isExactKeyMatch with different key accents returns false", () => {
|
||||
const key = "linux-áccent";
|
||||
const cacheEntry: ArtifactCacheEntry = {
|
||||
cacheKey: "linux-accent"
|
||||
};
|
||||
const cacheKey = "linux-accent";
|
||||
|
||||
expect(actionUtils.isExactKeyMatch(key, cacheEntry)).toBe(false);
|
||||
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(false);
|
||||
});
|
||||
|
||||
test("isExactKeyMatch with same key returns true", () => {
|
||||
const key = "linux-rust";
|
||||
const cacheEntry: ArtifactCacheEntry = {
|
||||
cacheKey: "linux-rust"
|
||||
};
|
||||
const cacheKey = "linux-rust";
|
||||
|
||||
expect(actionUtils.isExactKeyMatch(key, cacheEntry)).toBe(true);
|
||||
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(true);
|
||||
});
|
||||
|
||||
test("isExactKeyMatch with same key and different casing returns true", () => {
|
||||
const key = "linux-rust";
|
||||
const cacheEntry: ArtifactCacheEntry = {
|
||||
cacheKey: "LINUX-RUST"
|
||||
};
|
||||
const cacheKey = "LINUX-RUST";
|
||||
|
||||
expect(actionUtils.isExactKeyMatch(key, cacheEntry)).toBe(true);
|
||||
expect(actionUtils.isExactKeyMatch(key, cacheKey)).toBe(true);
|
||||
});
|
||||
|
||||
test("setOutputAndState with undefined entry to set cache-hit output", () => {
|
||||
const key = "linux-rust";
|
||||
const cacheEntry = undefined;
|
||||
const cacheKey = undefined;
|
||||
|
||||
const setOutputMock = jest.spyOn(core, "setOutput");
|
||||
const saveStateMock = jest.spyOn(core, "saveState");
|
||||
|
||||
actionUtils.setOutputAndState(key, cacheEntry);
|
||||
actionUtils.setOutputAndState(key, cacheKey);
|
||||
|
||||
expect(setOutputMock).toHaveBeenCalledWith(Outputs.CacheHit, "false");
|
||||
expect(setOutputMock).toHaveBeenCalledTimes(1);
|
||||
@ -88,43 +76,33 @@ test("setOutputAndState with undefined entry to set cache-hit output", () => {
|
||||
|
||||
test("setOutputAndState with exact match to set cache-hit output and state", () => {
|
||||
const key = "linux-rust";
|
||||
const cacheEntry: ArtifactCacheEntry = {
|
||||
cacheKey: "linux-rust"
|
||||
};
|
||||
const cacheKey = "linux-rust";
|
||||
|
||||
const setOutputMock = jest.spyOn(core, "setOutput");
|
||||
const saveStateMock = jest.spyOn(core, "saveState");
|
||||
|
||||
actionUtils.setOutputAndState(key, cacheEntry);
|
||||
actionUtils.setOutputAndState(key, cacheKey);
|
||||
|
||||
expect(setOutputMock).toHaveBeenCalledWith(Outputs.CacheHit, "true");
|
||||
expect(setOutputMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(saveStateMock).toHaveBeenCalledWith(
|
||||
State.CacheResult,
|
||||
JSON.stringify(cacheEntry)
|
||||
);
|
||||
expect(saveStateMock).toHaveBeenCalledWith(State.CacheMatchedKey, cacheKey);
|
||||
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 cacheKey = "linux-rust-bb828da54c148048dd17899ba9fda624811cfb43";
|
||||
|
||||
const setOutputMock = jest.spyOn(core, "setOutput");
|
||||
const saveStateMock = jest.spyOn(core, "saveState");
|
||||
|
||||
actionUtils.setOutputAndState(key, cacheEntry);
|
||||
actionUtils.setOutputAndState(key, cacheKey);
|
||||
|
||||
expect(setOutputMock).toHaveBeenCalledWith(Outputs.CacheHit, "false");
|
||||
expect(setOutputMock).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(saveStateMock).toHaveBeenCalledWith(
|
||||
State.CacheResult,
|
||||
JSON.stringify(cacheEntry)
|
||||
);
|
||||
expect(saveStateMock).toHaveBeenCalledWith(State.CacheMatchedKey, cacheKey);
|
||||
expect(saveStateMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@ -138,27 +116,23 @@ test("getCacheState with no state returns undefined", () => {
|
||||
|
||||
expect(state).toBe(undefined);
|
||||
|
||||
expect(getStateMock).toHaveBeenCalledWith(State.CacheResult);
|
||||
expect(getStateMock).toHaveBeenCalledWith(State.CacheMatchedKey);
|
||||
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 cacheKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
||||
|
||||
const getStateMock = jest.spyOn(core, "getState");
|
||||
getStateMock.mockImplementation(() => {
|
||||
return JSON.stringify(cacheEntry);
|
||||
return cacheKey;
|
||||
});
|
||||
|
||||
const state = actionUtils.getCacheState();
|
||||
|
||||
expect(state).toEqual(cacheEntry);
|
||||
expect(state).toEqual(cacheKey);
|
||||
|
||||
expect(getStateMock).toHaveBeenCalledWith(State.CacheResult);
|
||||
expect(getStateMock).toHaveBeenCalledWith(State.CacheMatchedKey);
|
||||
expect(getStateMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@ -172,7 +146,7 @@ test("logWarning logs a message with a warning prefix", () => {
|
||||
expect(infoMock).toHaveBeenCalledWith(`[warning]${message}`);
|
||||
});
|
||||
|
||||
test("isValidEvent returns false for unknown event", () => {
|
||||
test("isValidEvent returns false for event that does not have a branch or tag", () => {
|
||||
const event = "foo";
|
||||
process.env[Events.Key] = event;
|
||||
|
||||
@ -181,56 +155,42 @@ test("isValidEvent returns false for unknown event", () => {
|
||||
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", () => {
|
||||
test("isValidEvent returns true for event that has a ref", () => {
|
||||
const event = Events.Push;
|
||||
process.env[Events.Key] = event;
|
||||
process.env[RefKey] = "ref/heads/feature";
|
||||
|
||||
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);
|
||||
test("getInputAsArray returns empty array if not required and missing", () => {
|
||||
expect(actionUtils.getInputAsArray("foo")).toEqual([]);
|
||||
});
|
||||
|
||||
test("getInputAsArray throws error if required and missing", () => {
|
||||
expect(() =>
|
||||
actionUtils.getInputAsArray("foo", { required: true })
|
||||
).toThrowError();
|
||||
});
|
||||
|
||||
test("getInputAsArray handles single line correctly", () => {
|
||||
testUtils.setInput("foo", "bar");
|
||||
expect(actionUtils.getInputAsArray("foo")).toEqual(["bar"]);
|
||||
});
|
||||
|
||||
test("getInputAsArray handles multiple lines correctly", () => {
|
||||
testUtils.setInput("foo", "bar\nbaz");
|
||||
expect(actionUtils.getInputAsArray("foo")).toEqual(["bar", "baz"]);
|
||||
});
|
||||
|
||||
test("getInputAsArray handles different new lines correctly", () => {
|
||||
testUtils.setInput("foo", "bar\r\nbaz");
|
||||
expect(actionUtils.getInputAsArray("foo")).toEqual(["bar", "baz"]);
|
||||
});
|
||||
|
||||
test("getInputAsArray handles empty lines correctly", () => {
|
||||
testUtils.setInput("foo", "\n\nbar\n\nbaz\n\n");
|
||||
expect(actionUtils.getInputAsArray("foo")).toEqual(["bar", "baz"]);
|
||||
});
|
||||
|
17
__tests__/create-cache-files.sh
Executable file
17
__tests__/create-cache-files.sh
Executable file
@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Validate args
|
||||
prefix="$1"
|
||||
if [ -z "$prefix" ]; then
|
||||
echo "Must supply prefix argument"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
path="$2"
|
||||
if [ -z "$path" ]; then
|
||||
echo "Must supply path argument"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p $path
|
||||
echo "$prefix $GITHUB_RUN_ID" > $path/test-file.txt
|
@ -1,22 +1,14 @@
|
||||
import * as cache from "@actions/cache";
|
||||
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 { Events, Inputs, RefKey } from "../src/constants";
|
||||
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");
|
||||
@ -29,19 +21,23 @@ beforeAll(() => {
|
||||
return actualUtils.isValidEvent();
|
||||
});
|
||||
|
||||
jest.spyOn(actionUtils, "getSupportedEvents").mockImplementation(() => {
|
||||
const actualUtils = jest.requireActual("../src/utils/actionUtils");
|
||||
return actualUtils.getSupportedEvents();
|
||||
});
|
||||
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
|
||||
(name, options) => {
|
||||
const actualUtils = jest.requireActual("../src/utils/actionUtils");
|
||||
return actualUtils.getInputAsArray(name, options);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
process.env[Events.Key] = Events.Push;
|
||||
process.env[RefKey] = "refs/heads/feature-branch";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
testUtils.clearInputs();
|
||||
delete process.env[Events.Key];
|
||||
delete process.env[RefKey];
|
||||
});
|
||||
|
||||
test("restore with invalid event outputs warning", async () => {
|
||||
@ -49,17 +45,21 @@ test("restore with invalid event outputs warning", async () => {
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
const invalidEvent = "commit_comment";
|
||||
process.env[Events.Key] = invalidEvent;
|
||||
delete process.env[RefKey];
|
||||
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.`
|
||||
`Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
|
||||
);
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test("restore with no path should fail", async () => {
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
||||
await run();
|
||||
expect(failedMock).toHaveBeenCalledWith(
|
||||
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
||||
// this input isn't necessary for restore b/c tarball contains entries relative to workspace
|
||||
expect(failedMock).not.toHaveBeenCalledWith(
|
||||
"Input required and not supplied: path"
|
||||
);
|
||||
});
|
||||
@ -67,99 +67,120 @@ test("restore with no path should fail", async () => {
|
||||
test("restore with no key", async () => {
|
||||
testUtils.setInput(Inputs.Path, "node_modules");
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
||||
await run();
|
||||
expect(restoreCacheMock).toHaveBeenCalledTimes(0);
|
||||
expect(failedMock).toHaveBeenCalledWith(
|
||||
"Input required and not supplied: key"
|
||||
);
|
||||
});
|
||||
|
||||
test("restore with too many keys should fail", async () => {
|
||||
const path = "node_modules";
|
||||
const key = "node-test";
|
||||
const restoreKeys = [...Array(20).keys()].map(x => x.toString());
|
||||
testUtils.setInputs({
|
||||
path: "node_modules",
|
||||
path: path,
|
||||
key,
|
||||
restoreKeys
|
||||
});
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
||||
await run();
|
||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, restoreKeys);
|
||||
expect(failedMock).toHaveBeenCalledWith(
|
||||
`Key Validation Error: Keys are limited to a maximum of 10.`
|
||||
);
|
||||
});
|
||||
|
||||
test("restore with large key should fail", async () => {
|
||||
const path = "node_modules";
|
||||
const key = "foo".repeat(512); // Over the 512 character limit
|
||||
testUtils.setInputs({
|
||||
path: "node_modules",
|
||||
path: path,
|
||||
key
|
||||
});
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
||||
await run();
|
||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
||||
expect(failedMock).toHaveBeenCalledWith(
|
||||
`Key Validation Error: ${key} cannot be larger than 512 characters.`
|
||||
);
|
||||
});
|
||||
|
||||
test("restore with invalid key should fail", async () => {
|
||||
const path = "node_modules";
|
||||
const key = "comma,comma";
|
||||
testUtils.setInputs({
|
||||
path: "node_modules",
|
||||
path: path,
|
||||
key
|
||||
});
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
const restoreCacheMock = jest.spyOn(cache, "restoreCache");
|
||||
await run();
|
||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
||||
expect(failedMock).toHaveBeenCalledWith(
|
||||
`Key Validation Error: ${key} cannot contain commas.`
|
||||
);
|
||||
});
|
||||
|
||||
test("restore with no cache found", async () => {
|
||||
const path = "node_modules";
|
||||
const key = "node-test";
|
||||
testUtils.setInputs({
|
||||
path: "node_modules",
|
||||
path: path,
|
||||
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);
|
||||
});
|
||||
const restoreCacheMock = jest
|
||||
.spyOn(cache, "restoreCache")
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
||||
|
||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(infoMock).toHaveBeenCalledWith(
|
||||
`Cache not found for input keys: ${key}.`
|
||||
`Cache not found for input keys: ${key}`
|
||||
);
|
||||
});
|
||||
|
||||
test("restore with server error should fail", async () => {
|
||||
const path = "node_modules";
|
||||
const key = "node-test";
|
||||
testUtils.setInputs({
|
||||
path: "node_modules",
|
||||
path: path,
|
||||
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 restoreCacheMock = jest
|
||||
.spyOn(cache, "restoreCache")
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error("HTTP Error Occurred");
|
||||
});
|
||||
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
|
||||
|
||||
await run();
|
||||
|
||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
||||
|
||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||
|
||||
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
||||
@ -172,10 +193,11 @@ test("restore with server error should fail", async () => {
|
||||
});
|
||||
|
||||
test("restore with restore keys and no cache found", async () => {
|
||||
const path = "node_modules";
|
||||
const key = "node-test";
|
||||
const restoreKey = "node-";
|
||||
testUtils.setInputs({
|
||||
path: "node_modules",
|
||||
path: path,
|
||||
key,
|
||||
restoreKeys: [restoreKey]
|
||||
});
|
||||
@ -183,148 +205,49 @@ test("restore with restore keys and no cache found", async () => {
|
||||
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);
|
||||
});
|
||||
const restoreCacheMock = jest
|
||||
.spyOn(cache, "restoreCache")
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve(undefined);
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, [restoreKey]);
|
||||
|
||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(infoMock).toHaveBeenCalledWith(
|
||||
`Cache not found for input keys: ${key}, ${restoreKey}.`
|
||||
`Cache not found for input keys: ${key}, ${restoreKey}`
|
||||
);
|
||||
});
|
||||
|
||||
test("restore with cache found", async () => {
|
||||
test("restore with cache found for key", async () => {
|
||||
const path = "node_modules";
|
||||
const key = "node-test";
|
||||
const cachePath = path.resolve("node_modules");
|
||||
testUtils.setInputs({
|
||||
path: "node_modules",
|
||||
path: path,
|
||||
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");
|
||||
const restoreCacheMock = jest
|
||||
.spyOn(cache, "restoreCache")
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve(key);
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||
expect(getCacheMock).toHaveBeenCalledWith([key]);
|
||||
expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry);
|
||||
expect(createTempDirectoryMock).toHaveBeenCalledTimes(1);
|
||||
expect(downloadCacheMock).toHaveBeenCalledWith(
|
||||
cacheEntry.archiveLocation,
|
||||
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(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, []);
|
||||
|
||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||
expect(getCacheMock).toHaveBeenCalledWith([key]);
|
||||
expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry);
|
||||
expect(createTempDirectoryMock).toHaveBeenCalledTimes(1);
|
||||
expect(downloadCacheMock).toHaveBeenCalledWith(
|
||||
cacheEntry.archiveLocation,
|
||||
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);
|
||||
|
||||
@ -333,11 +256,11 @@ test("restore with a pull request event and cache found", async () => {
|
||||
});
|
||||
|
||||
test("restore with cache found for restore key", async () => {
|
||||
const path = "node_modules";
|
||||
const key = "node-test";
|
||||
const restoreKey = "node-";
|
||||
const cachePath = path.resolve("node_modules");
|
||||
testUtils.setInputs({
|
||||
path: "node_modules",
|
||||
path: path,
|
||||
key,
|
||||
restoreKeys: [restoreKey]
|
||||
});
|
||||
@ -345,54 +268,19 @@ test("restore with cache found for restore key", async () => {
|
||||
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");
|
||||
const restoreCacheMock = jest
|
||||
.spyOn(cache, "restoreCache")
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve(restoreKey);
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(restoreCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(restoreCacheMock).toHaveBeenCalledWith([path], key, [restoreKey]);
|
||||
|
||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||
expect(getCacheMock).toHaveBeenCalledWith([key, restoreKey]);
|
||||
expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry);
|
||||
expect(createTempDirectoryMock).toHaveBeenCalledTimes(1);
|
||||
expect(downloadCacheMock).toHaveBeenCalledWith(
|
||||
cacheEntry.archiveLocation,
|
||||
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);
|
||||
|
||||
|
@ -1,16 +1,13 @@
|
||||
import * as cache from "@actions/cache";
|
||||
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 { Events, Inputs, RefKey } from "../src/constants";
|
||||
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("@actions/cache");
|
||||
jest.mock("../src/utils/actionUtils");
|
||||
|
||||
beforeAll(() => {
|
||||
@ -22,6 +19,14 @@ beforeAll(() => {
|
||||
return jest.requireActual("../src/utils/actionUtils").getCacheState();
|
||||
});
|
||||
|
||||
jest.spyOn(actionUtils, "getInputAsArray").mockImplementation(
|
||||
(name, options) => {
|
||||
return jest
|
||||
.requireActual("../src/utils/actionUtils")
|
||||
.getInputAsArray(name, options);
|
||||
}
|
||||
);
|
||||
|
||||
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
|
||||
(key, cacheResult) => {
|
||||
return jest
|
||||
@ -34,28 +39,17 @@ beforeAll(() => {
|
||||
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;
|
||||
process.env[RefKey] = "refs/heads/feature-branch";
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
testUtils.clearInputs();
|
||||
delete process.env[Events.Key];
|
||||
delete process.env[RefKey];
|
||||
});
|
||||
|
||||
test("save with invalid event outputs warning", async () => {
|
||||
@ -63,9 +57,10 @@ test("save with invalid event outputs warning", async () => {
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
const invalidEvent = "commit_comment";
|
||||
process.env[Events.Key] = invalidEvent;
|
||||
delete process.env[RefKey];
|
||||
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.`
|
||||
`Event Validation Error: The event type ${invalidEvent} is not supported because it's not tied to a branch or tag ref.`
|
||||
);
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
@ -74,25 +69,21 @@ 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"
|
||||
};
|
||||
|
||||
const savedCacheKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
||||
jest.spyOn(core, "getState")
|
||||
// Cache Entry State
|
||||
.mockImplementationOnce(() => {
|
||||
return JSON.stringify(cacheEntry);
|
||||
return savedCacheKey;
|
||||
})
|
||||
// Cache Key State
|
||||
.mockImplementationOnce(() => {
|
||||
return "";
|
||||
});
|
||||
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
||||
|
||||
await run();
|
||||
|
||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||
expect(logWarningMock).toHaveBeenCalledWith(
|
||||
`Error retrieving key from state.`
|
||||
);
|
||||
@ -105,33 +96,25 @@ test("save with exact match returns early", async () => {
|
||||
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"
|
||||
};
|
||||
const savedCacheKey = primaryKey;
|
||||
|
||||
jest.spyOn(core, "getState")
|
||||
// Cache Entry State
|
||||
.mockImplementationOnce(() => {
|
||||
return JSON.stringify(cacheEntry);
|
||||
return savedCacheKey;
|
||||
})
|
||||
// Cache Key State
|
||||
.mockImplementationOnce(() => {
|
||||
return primaryKey;
|
||||
});
|
||||
|
||||
const createTarMock = jest.spyOn(tar, "createTar");
|
||||
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
||||
|
||||
await run();
|
||||
|
||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||
expect(infoMock).toHaveBeenCalledWith(
|
||||
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
|
||||
);
|
||||
|
||||
expect(createTarMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
@ -140,25 +123,22 @@ test("save with missing input outputs warning", 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"
|
||||
};
|
||||
const savedCacheKey = "Linux-node-";
|
||||
|
||||
jest.spyOn(core, "getState")
|
||||
// Cache Entry State
|
||||
.mockImplementationOnce(() => {
|
||||
return JSON.stringify(cacheEntry);
|
||||
return savedCacheKey;
|
||||
})
|
||||
// Cache Key State
|
||||
.mockImplementationOnce(() => {
|
||||
return primaryKey;
|
||||
});
|
||||
const saveCacheMock = jest.spyOn(cache, "saveCache");
|
||||
|
||||
await run();
|
||||
|
||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||
expect(logWarningMock).toHaveBeenCalledWith(
|
||||
"Input required and not supplied: path"
|
||||
);
|
||||
@ -171,17 +151,12 @@ test("save with large cache outputs warning", 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"
|
||||
};
|
||||
const savedCacheKey = "Linux-node-";
|
||||
|
||||
jest.spyOn(core, "getState")
|
||||
// Cache Entry State
|
||||
.mockImplementationOnce(() => {
|
||||
return JSON.stringify(cacheEntry);
|
||||
return savedCacheKey;
|
||||
})
|
||||
// Cache Key State
|
||||
.mockImplementationOnce(() => {
|
||||
@ -189,28 +164,25 @@ test("save with large cache outputs warning", async () => {
|
||||
});
|
||||
|
||||
const inputPath = "node_modules";
|
||||
const cachePath = path.resolve(inputPath);
|
||||
testUtils.setInput(Inputs.Path, inputPath);
|
||||
|
||||
const createTarMock = jest.spyOn(tar, "createTar");
|
||||
|
||||
const cacheSize = 6 * 1024 * 1024 * 1024; //~6GB, over the 5GB limit
|
||||
jest.spyOn(actionUtils, "getArchiveFileSize").mockImplementationOnce(() => {
|
||||
return cacheSize;
|
||||
});
|
||||
const saveCacheMock = jest
|
||||
.spyOn(cache, "saveCache")
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error(
|
||||
"Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
|
||||
);
|
||||
});
|
||||
|
||||
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([inputPath], primaryKey);
|
||||
|
||||
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
||||
expect(logWarningMock).toHaveBeenCalledWith(
|
||||
"Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
|
||||
);
|
||||
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
@ -220,17 +192,12 @@ test("save with reserve cache failure outputs warning", 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"
|
||||
};
|
||||
const savedCacheKey = "Linux-node-";
|
||||
|
||||
jest.spyOn(core, "getState")
|
||||
// Cache Entry State
|
||||
.mockImplementationOnce(() => {
|
||||
return JSON.stringify(cacheEntry);
|
||||
return savedCacheKey;
|
||||
})
|
||||
// Cache Key State
|
||||
.mockImplementationOnce(() => {
|
||||
@ -240,27 +207,24 @@ test("save with reserve cache failure outputs warning", async () => {
|
||||
const inputPath = "node_modules";
|
||||
testUtils.setInput(Inputs.Path, inputPath);
|
||||
|
||||
const reserveCacheMock = jest
|
||||
.spyOn(cacheHttpClient, "reserveCache")
|
||||
const saveCacheMock = jest
|
||||
.spyOn(cache, "saveCache")
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve(-1);
|
||||
const actualCache = jest.requireActual("@actions/cache");
|
||||
const error = new actualCache.ReserveCacheError(
|
||||
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
|
||||
);
|
||||
throw error;
|
||||
});
|
||||
|
||||
const createTarMock = jest.spyOn(tar, "createTar");
|
||||
|
||||
const saveCacheMock = jest.spyOn(cacheHttpClient, "saveCache");
|
||||
|
||||
await run();
|
||||
|
||||
expect(reserveCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey);
|
||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(saveCacheMock).toHaveBeenCalledWith([inputPath], primaryKey);
|
||||
|
||||
expect(infoMock).toHaveBeenCalledWith(
|
||||
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
|
||||
);
|
||||
|
||||
expect(createTarMock).toHaveBeenCalledTimes(0);
|
||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||
expect(logWarningMock).toHaveBeenCalledTimes(0);
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
@ -270,17 +234,12 @@ test("save with server error outputs warning", 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"
|
||||
};
|
||||
const savedCacheKey = "Linux-node-";
|
||||
|
||||
jest.spyOn(core, "getState")
|
||||
// Cache Entry State
|
||||
.mockImplementationOnce(() => {
|
||||
return JSON.stringify(cacheEntry);
|
||||
return savedCacheKey;
|
||||
})
|
||||
// Cache Key State
|
||||
.mockImplementationOnce(() => {
|
||||
@ -288,36 +247,18 @@ test("save with server error outputs warning", async () => {
|
||||
});
|
||||
|
||||
const inputPath = "node_modules";
|
||||
const cachePath = path.resolve(inputPath);
|
||||
testUtils.setInput(Inputs.Path, inputPath);
|
||||
|
||||
const cacheId = 4;
|
||||
const reserveCacheMock = jest
|
||||
.spyOn(cacheHttpClient, "reserveCache")
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve(cacheId);
|
||||
});
|
||||
|
||||
const createTarMock = jest.spyOn(tar, "createTar");
|
||||
|
||||
const saveCacheMock = jest
|
||||
.spyOn(cacheHttpClient, "saveCache")
|
||||
.spyOn(cache, "saveCache")
|
||||
.mockImplementationOnce(() => {
|
||||
throw new Error("HTTP Error Occurred");
|
||||
});
|
||||
|
||||
await run();
|
||||
|
||||
expect(reserveCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey);
|
||||
|
||||
const archivePath = path.join("/foo/bar", "cache.tgz");
|
||||
|
||||
expect(createTarMock).toHaveBeenCalledTimes(1);
|
||||
expect(createTarMock).toHaveBeenCalledWith(archivePath, cachePath);
|
||||
|
||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archivePath);
|
||||
expect(saveCacheMock).toHaveBeenCalledWith([inputPath], primaryKey);
|
||||
|
||||
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
||||
expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
|
||||
@ -329,17 +270,12 @@ 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"
|
||||
};
|
||||
const savedCacheKey = "Linux-node-";
|
||||
|
||||
jest.spyOn(core, "getState")
|
||||
// Cache Entry State
|
||||
.mockImplementationOnce(() => {
|
||||
return JSON.stringify(cacheEntry);
|
||||
return savedCacheKey;
|
||||
})
|
||||
// Cache Key State
|
||||
.mockImplementationOnce(() => {
|
||||
@ -347,32 +283,19 @@ test("save with valid inputs uploads a cache", async () => {
|
||||
});
|
||||
|
||||
const inputPath = "node_modules";
|
||||
const cachePath = path.resolve(inputPath);
|
||||
testUtils.setInput(Inputs.Path, inputPath);
|
||||
|
||||
const cacheId = 4;
|
||||
const reserveCacheMock = jest
|
||||
.spyOn(cacheHttpClient, "reserveCache")
|
||||
const saveCacheMock = jest
|
||||
.spyOn(cache, "saveCache")
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve(cacheId);
|
||||
});
|
||||
|
||||
const createTarMock = jest.spyOn(tar, "createTar");
|
||||
|
||||
const saveCacheMock = jest.spyOn(cacheHttpClient, "saveCache");
|
||||
|
||||
await run();
|
||||
|
||||
expect(reserveCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey);
|
||||
|
||||
const archivePath = path.join("/foo/bar", "cache.tgz");
|
||||
|
||||
expect(createTarMock).toHaveBeenCalledTimes(1);
|
||||
expect(createTarMock).toHaveBeenCalledWith(archivePath, cachePath);
|
||||
|
||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archivePath);
|
||||
expect(saveCacheMock).toHaveBeenCalledWith([inputPath], primaryKey);
|
||||
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
@ -1,58 +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(() => {
|
||||
jest.spyOn(io, "which").mockImplementation(tool => {
|
||||
return Promise.resolve(tool);
|
||||
});
|
||||
});
|
||||
|
||||
test("extract tar", async () => {
|
||||
const mkdirMock = jest.spyOn(io, "mkdirP");
|
||||
const execMock = jest.spyOn(exec, "exec");
|
||||
|
||||
const archivePath = "cache.tar";
|
||||
const targetDirectory = "~/.npm/cache";
|
||||
await tar.extractTar(archivePath, targetDirectory);
|
||||
|
||||
expect(mkdirMock).toHaveBeenCalledWith(targetDirectory);
|
||||
|
||||
const IS_WINDOWS = process.platform === "win32";
|
||||
const tarPath = IS_WINDOWS
|
||||
? `${process.env["windir"]}\\System32\\tar.exe`
|
||||
: "tar";
|
||||
expect(execMock).toHaveBeenCalledTimes(1);
|
||||
expect(execMock).toHaveBeenCalledWith(`"${tarPath}"`, [
|
||||
"-xz",
|
||||
"-f",
|
||||
archivePath,
|
||||
"-C",
|
||||
targetDirectory
|
||||
]);
|
||||
});
|
||||
|
||||
test("create tar", async () => {
|
||||
const execMock = jest.spyOn(exec, "exec");
|
||||
|
||||
const archivePath = "cache.tar";
|
||||
const sourceDirectory = "~/.npm/cache";
|
||||
await tar.createTar(archivePath, sourceDirectory);
|
||||
|
||||
const IS_WINDOWS = process.platform === "win32";
|
||||
const tarPath = IS_WINDOWS
|
||||
? `${process.env["windir"]}\\System32\\tar.exe`
|
||||
: "tar";
|
||||
expect(execMock).toHaveBeenCalledTimes(1);
|
||||
expect(execMock).toHaveBeenCalledWith(`"${tarPath}"`, [
|
||||
"-cz",
|
||||
"-f",
|
||||
archivePath,
|
||||
"-C",
|
||||
sourceDirectory,
|
||||
"."
|
||||
]);
|
||||
});
|
36
__tests__/verify-cache-files.sh
Executable file
36
__tests__/verify-cache-files.sh
Executable file
@ -0,0 +1,36 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Validate args
|
||||
prefix="$1"
|
||||
if [ -z "$prefix" ]; then
|
||||
echo "Must supply prefix argument"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
path="$2"
|
||||
if [ -z "$path" ]; then
|
||||
echo "Must specify path argument"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Sanity check GITHUB_RUN_ID defined
|
||||
if [ -z "$GITHUB_RUN_ID" ]; then
|
||||
echo "GITHUB_RUN_ID not defined"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify file exists
|
||||
file="$path/test-file.txt"
|
||||
echo "Checking for $file"
|
||||
if [ ! -e $file ]; then
|
||||
echo "File does not exist"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify file content
|
||||
content="$(cat $file)"
|
||||
echo "File content:\n$content"
|
||||
if [ -z "$(echo $content | grep --fixed-strings "$prefix $GITHUB_RUN_ID")" ]; then
|
||||
echo "Unexpected file content"
|
||||
exit 1
|
||||
fi
|
@ -3,7 +3,7 @@ description: 'Cache artifacts like dependencies and build outputs to improve wor
|
||||
author: 'GitHub'
|
||||
inputs:
|
||||
path:
|
||||
description: 'A directory to store and save the cache'
|
||||
description: 'A list of files, directories, and wildcard patterns to cache and restore'
|
||||
required: true
|
||||
key:
|
||||
description: 'An explicit key for restoring and saving the cache'
|
||||
|
48990
dist/restore/index.js
vendored
Normal file
48990
dist/restore/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
48987
dist/save/index.js
vendored
Normal file
48987
dist/save/index.js
vendored
Normal file
File diff suppressed because one or more lines are too long
312
examples.md
312
examples.md
@ -1,28 +1,42 @@
|
||||
# Examples
|
||||
|
||||
- [C# - NuGet](#c---nuget)
|
||||
- [Elixir - Mix](#elixir---mix)
|
||||
- [Go - Modules](#go---modules)
|
||||
- [Haskell - Cabal](#haskell---cabal)
|
||||
- [Java - Gradle](#java---gradle)
|
||||
- [Java - Maven](#java---maven)
|
||||
- [Node - npm](#node---npm)
|
||||
- [Node - Yarn](#node---yarn)
|
||||
- [PHP - Composer](#php---composer)
|
||||
- [Python - pip](#python---pip)
|
||||
- [R - renv](#r---renv)
|
||||
- [Ruby - Bundler](#ruby---bundler)
|
||||
- [Rust - Cargo](#rust---cargo)
|
||||
- [Scala - SBT](#scala---sbt)
|
||||
- [Swift, Objective-C - Carthage](#swift-objective-c---carthage)
|
||||
- [Swift, Objective-C - CocoaPods](#swift-objective-c---cocoapods)
|
||||
- [Swift - Swift Package Manager](#swift---swift-package-manager)
|
||||
- [Examples](#examples)
|
||||
- [C# - NuGet](#c---nuget)
|
||||
- [D - DUB](#d---dub)
|
||||
- [Elixir - Mix](#elixir---mix)
|
||||
- [Go - Modules](#go---modules)
|
||||
- [Haskell - Cabal](#haskell---cabal)
|
||||
- [Java - Gradle](#java---gradle)
|
||||
- [Java - Maven](#java---maven)
|
||||
- [Node - npm](#node---npm)
|
||||
- [macOS and Ubuntu](#macos-and-ubuntu)
|
||||
- [Windows](#windows)
|
||||
- [Using multiple systems and `npm config`](#using-multiple-systems-and-npm-config)
|
||||
- [Node - Lerna](#node---lerna)
|
||||
- [Node - Yarn](#node---yarn)
|
||||
- [Node - Yarn 2](#node---yarn-2)
|
||||
- [OCaml/Reason - esy](#ocamlreason---esy)
|
||||
- [PHP - Composer](#php---composer)
|
||||
- [Python - pip](#python---pip)
|
||||
- [Simple example](#simple-example)
|
||||
- [Multiple OS's in a workflow](#multiple-oss-in-a-workflow)
|
||||
- [Using pip to get cache location](#using-pip-to-get-cache-location)
|
||||
- [Using a script to get cache location](#using-a-script-to-get-cache-location)
|
||||
- [R - renv](#r---renv)
|
||||
- [Simple example](#simple-example-1)
|
||||
- [Multiple OS's in a workflow](#multiple-oss-in-a-workflow-1)
|
||||
- [Ruby - Bundler](#ruby---bundler)
|
||||
- [Rust - Cargo](#rust---cargo)
|
||||
- [Scala - SBT](#scala---sbt)
|
||||
- [Swift, Objective-C - Carthage](#swift-objective-c---carthage)
|
||||
- [Swift, Objective-C - CocoaPods](#swift-objective-c---cocoapods)
|
||||
- [Swift - Swift Package Manager](#swift---swift-package-manager)
|
||||
|
||||
## C# - NuGet
|
||||
Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/package-references-in-project-files#locking-dependencies):
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
|
||||
@ -31,13 +45,25 @@ Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/packa
|
||||
```
|
||||
|
||||
Depending on the environment, huge packages might be pre-installed in the global cache folder.
|
||||
If you do not want to include them, consider to move the cache folder like below.
|
||||
With `actions/cache@v2` you can now exclude unwanted packages with [exclude pattern](https://github.com/actions/toolkit/tree/main/packages/glob#exclude-patterns)
|
||||
```yaml
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.nuget/packages
|
||||
!~/.nuget/packages/unwanted
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-
|
||||
```
|
||||
|
||||
Or you could move the cache folder like below.
|
||||
>Note: This workflow does not work for projects that require files to be placed in user profile package folder
|
||||
```yaml
|
||||
env:
|
||||
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
||||
steps:
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ github.workspace }}/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
|
||||
@ -45,9 +71,33 @@ steps:
|
||||
${{ runner.os }}-nuget-
|
||||
```
|
||||
|
||||
## D - DUB
|
||||
|
||||
### POSIX
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.dub
|
||||
key: ${{ runner.os }}-dub-${{ hashFiles('**/dub.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-dub-
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~\AppData\Local\dub
|
||||
key: ${{ runner.os }}-dub-${{ hashFiles('**/dub.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-dub-
|
||||
```
|
||||
|
||||
## Elixir - Mix
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: deps
|
||||
key: ${{ runner.os }}-mix-${{ hashFiles(format('{0}{1}', github.workspace, '/mix.lock')) }}
|
||||
@ -58,7 +108,7 @@ steps:
|
||||
## Go - Modules
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
@ -71,30 +121,25 @@ steps:
|
||||
We cache the elements of the Cabal store separately, as the entirety of `~/.cabal` can grow very large for projects with many dependencies.
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
name: Cache ~/.cabal/packages
|
||||
- name: Cache ~/.cabal/packages, ~/.cabal/store and dist-newstyle
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cabal/packages
|
||||
key: ${{ runner.os }}-${{ matrix.ghc }}-cabal-packages
|
||||
- uses: actions/cache@v1
|
||||
name: Cache ~/.cabal/store
|
||||
with:
|
||||
path: ~/.cabal/store
|
||||
key: ${{ runner.os }}-${{ matrix.ghc }}-cabal-store
|
||||
- uses: actions/cache@v1
|
||||
name: Cache dist-newstyle
|
||||
with:
|
||||
path: dist-newstyle
|
||||
key: ${{ runner.os }}-${{ matrix.ghc }}-dist-newstyle
|
||||
path: |
|
||||
~/.cabal/packages
|
||||
~/.cabal/store
|
||||
dist-newstyle
|
||||
key: ${{ runner.os }}-${{ matrix.ghc }}
|
||||
```
|
||||
|
||||
## Java - Gradle
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.gradle/caches
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
|
||||
path: |
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gradle-
|
||||
```
|
||||
@ -102,7 +147,8 @@ We cache the elements of the Cabal store separately, as the entirety of `~/.caba
|
||||
## Java - Maven
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
- name: Cache local Maven repository
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.m2/repository
|
||||
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
|
||||
@ -114,12 +160,14 @@ We cache the elements of the Cabal store separately, as the entirety of `~/.caba
|
||||
|
||||
For npm, cache files are stored in `~/.npm` on Posix, or `%AppData%/npm-cache` on Windows. See https://docs.npmjs.com/cli/cache#cache
|
||||
|
||||
If using `npm config` to retrieve the cache directory, ensure you run [actions/setup-node](https://github.com/actions/setup-node) first to ensure your `npm` version is correct.
|
||||
|
||||
>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
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
@ -130,22 +178,11 @@ For npm, cache files are stored in `~/.npm` on Posix, or `%AppData%/npm-cache` o
|
||||
### 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
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.npm-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
@ -153,30 +190,104 @@ For npm, cache files are stored in `~/.npm` on Posix, or `%AppData%/npm-cache` o
|
||||
${{ 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@v2
|
||||
with:
|
||||
path: ${{ steps.npm-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
```
|
||||
|
||||
## Node - Lerna
|
||||
|
||||
```yaml
|
||||
- name: restore lerna
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
node_modules
|
||||
*/*/node_modules
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}
|
||||
```
|
||||
|
||||
## 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
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache.outputs.dir }}
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
```
|
||||
|
||||
|
||||
## Node - Yarn 2
|
||||
The yarn 2 cache directory will depend on your config. See https://yarnpkg.com/configuration/yarnrc#cacheFolder for more info.
|
||||
|
||||
```yaml
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn config get cacheFolder)"
|
||||
|
||||
- uses: actions/cache@v2
|
||||
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
```
|
||||
|
||||
## OCaml/Reason - esy
|
||||
Esy allows you to export built dependencies and import pre-built dependencies.
|
||||
```yaml
|
||||
- name: Restore Cache
|
||||
id: restore-cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: _export
|
||||
key: ${{ runner.os }}-esy-${{ hashFiles('esy.lock/index.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-esy-
|
||||
- name: Esy install
|
||||
run: 'esy install'
|
||||
- name: Import Cache
|
||||
run: |
|
||||
esy import-dependencies _export
|
||||
rm -rf _export
|
||||
|
||||
...(Build job)...
|
||||
|
||||
# Re-export dependencies if anything has changed or if it is the first time
|
||||
- name: Setting dependency cache
|
||||
run: |
|
||||
esy export-dependencies
|
||||
if: steps.restore-cache.outputs.cache-hit != 'true'
|
||||
```
|
||||
|
||||
|
||||
## PHP - Composer
|
||||
|
||||
```yaml
|
||||
```yaml
|
||||
- name: Get Composer Cache Directory
|
||||
id: composer-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.composer-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
|
||||
@ -195,7 +306,7 @@ Locations:
|
||||
|
||||
### Simple example
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
@ -208,7 +319,7 @@ Replace `~/.cache/pip` with the correct `path` if not using Ubuntu.
|
||||
### Multiple OS's in a workflow
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
if: startsWith(runner.os, 'Linux')
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
@ -216,7 +327,7 @@ Replace `~/.cache/pip` with the correct `path` if not using Ubuntu.
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
if: startsWith(runner.os, 'macOS')
|
||||
with:
|
||||
path: ~/Library/Caches/pip
|
||||
@ -224,7 +335,7 @@ Replace `~/.cache/pip` with the correct `path` if not using Ubuntu.
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
if: startsWith(runner.os, 'Windows')
|
||||
with:
|
||||
path: ~\AppData\Local\pip\Cache
|
||||
@ -233,16 +344,34 @@ Replace `~/.cache/pip` with the correct `path` if not using Ubuntu.
|
||||
${{ runner.os }}-pip-
|
||||
```
|
||||
|
||||
### Using pip to get cache location
|
||||
|
||||
> Note: This requires pip 20.1+
|
||||
```yaml
|
||||
- name: Get pip cache dir
|
||||
id: pip-cache
|
||||
run: |
|
||||
echo "::set-output name=dir::$(pip cache dir)"
|
||||
|
||||
- name: pip cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.pip-cache.outputs.dir }}
|
||||
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)"
|
||||
- name: Get pip cache dir
|
||||
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
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.pip-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
@ -261,7 +390,7 @@ Locations:
|
||||
|
||||
### Simple example
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.local/share/renv
|
||||
key: ${{ runner.os }}-renv-${{ hashFiles('**/renv.lock') }}
|
||||
@ -274,7 +403,7 @@ Replace `~/.local/share/renv` with the correct `path` if not using Ubuntu.
|
||||
### Multiple OS's in a workflow
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
if: startsWith(runner.os, 'Linux')
|
||||
with:
|
||||
path: ~/.local/share/renv
|
||||
@ -282,7 +411,7 @@ Replace `~/.local/share/renv` with the correct `path` if not using Ubuntu.
|
||||
restore-keys: |
|
||||
${{ runner.os }}-renv-
|
||||
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
if: startsWith(runner.os, 'macOS')
|
||||
with:
|
||||
path: ~/Library/Application Support/renv
|
||||
@ -290,7 +419,7 @@ Replace `~/.local/share/renv` with the correct `path` if not using Ubuntu.
|
||||
restore-keys: |
|
||||
${{ runner.os }}-renv-
|
||||
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
if: startsWith(runner.os, 'Windows')
|
||||
with:
|
||||
path: ~\AppData\Local\renv
|
||||
@ -302,7 +431,7 @@ Replace `~/.local/share/renv` with the correct `path` if not using Ubuntu.
|
||||
## Ruby - Bundler
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
|
||||
@ -321,42 +450,31 @@ When dependencies are installed later in the workflow, we must specify the same
|
||||
## Rust - Cargo
|
||||
|
||||
```yaml
|
||||
- name: Cache cargo registry
|
||||
uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
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') }}
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
```
|
||||
|
||||
## Scala - SBT
|
||||
|
||||
```yaml
|
||||
- name: Cache SBT ivy cache
|
||||
uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.ivy2/cache
|
||||
key: ${{ runner.os }}-sbt-ivy-cache-${{ hashFiles('**/build.sbt') }}
|
||||
- name: Cache SBT
|
||||
uses: actions/cache@v1
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.sbt
|
||||
path: |
|
||||
~/.ivy2/cache
|
||||
~/.sbt
|
||||
key: ${{ runner.os }}-sbt-${{ hashFiles('**/build.sbt') }}
|
||||
```
|
||||
|
||||
## Swift, Objective-C - Carthage
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: Carthage
|
||||
key: ${{ runner.os }}-carthage-${{ hashFiles('**/Cartfile.resolved') }}
|
||||
@ -367,7 +485,7 @@ When dependencies are installed later in the workflow, we must specify the same
|
||||
## Swift, Objective-C - CocoaPods
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: Pods
|
||||
key: ${{ runner.os }}-pods-${{ hashFiles('**/Podfile.lock') }}
|
||||
@ -378,7 +496,7 @@ When dependencies are installed later in the workflow, we must specify the same
|
||||
## Swift - Swift Package Manager
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
- uses: actions/cache@v2
|
||||
with:
|
||||
path: .build
|
||||
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
|
||||
|
4186
package-lock.json
generated
4186
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@ -1,16 +1,15 @@
|
||||
{
|
||||
"name": "cache",
|
||||
"version": "1.1.2",
|
||||
"version": "2.1.1",
|
||||
"private": true,
|
||||
"description": "Cache dependencies and build outputs",
|
||||
"main": "dist/restore/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build": "tsc && ncc build -o dist/restore src/restore.ts && ncc build -o dist/save src/save.ts",
|
||||
"test": "tsc --noEmit && jest --coverage",
|
||||
"lint": "eslint **/*.ts --cache",
|
||||
"format": "prettier --write **/*.ts",
|
||||
"format-check": "prettier --check **/*.ts",
|
||||
"release": "ncc build -o dist/restore src/restore.ts && ncc build -o dist/save src/save.ts && git add -f dist/"
|
||||
"format-check": "prettier --check **/*.ts"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -26,15 +25,13 @@
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.0",
|
||||
"@actions/exec": "^1.0.1",
|
||||
"@actions/http-client": "^1.0.6",
|
||||
"@actions/io": "^1.0.1",
|
||||
"uuid": "^3.3.3"
|
||||
"@actions/cache": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^24.0.13",
|
||||
"@types/nock": "^11.1.0",
|
||||
"@types/node": "^12.0.4",
|
||||
"@types/uuid": "^3.4.5",
|
||||
"@typescript-eslint/eslint-plugin": "^2.7.0",
|
||||
"@typescript-eslint/parser": "^2.7.0",
|
||||
"@zeit/ncc": "^0.20.5",
|
||||
@ -43,6 +40,7 @@
|
||||
"eslint-plugin-import": "^2.18.2",
|
||||
"eslint-plugin-jest": "^23.0.3",
|
||||
"eslint-plugin-prettier": "^3.1.1",
|
||||
"eslint-plugin-simple-import-sort": "^5.0.2",
|
||||
"jest": "^24.8.0",
|
||||
"jest-circus": "^24.7.1",
|
||||
"nock": "^11.7.0",
|
||||
|
@ -1,298 +0,0 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as fs from "fs";
|
||||
import { BearerCredentialHandler } from "@actions/http-client/auth";
|
||||
import { HttpClient, HttpCodes } from "@actions/http-client";
|
||||
import {
|
||||
IHttpClientResponse,
|
||||
IRequestOptions,
|
||||
ITypedResponse
|
||||
} from "@actions/http-client/interfaces";
|
||||
import {
|
||||
ArtifactCacheEntry,
|
||||
CommitCacheRequest,
|
||||
ReserveCacheRequest,
|
||||
ReserveCacheResponse
|
||||
} from "./contracts";
|
||||
import * as utils from "./utils/actionUtils";
|
||||
|
||||
function isSuccessStatusCode(statusCode?: number): boolean {
|
||||
if (!statusCode) {
|
||||
return false;
|
||||
}
|
||||
return statusCode >= 200 && statusCode < 300;
|
||||
}
|
||||
|
||||
function isRetryableStatusCode(statusCode?: number): boolean {
|
||||
if (!statusCode) {
|
||||
return false;
|
||||
}
|
||||
const retryableStatusCodes = [
|
||||
HttpCodes.BadGateway,
|
||||
HttpCodes.ServiceUnavailable,
|
||||
HttpCodes.GatewayTimeout
|
||||
];
|
||||
return retryableStatusCodes.includes(statusCode);
|
||||
}
|
||||
|
||||
function getCacheApiUrl(resource: string): string {
|
||||
// Ideally we just use ACTIONS_CACHE_URL
|
||||
const baseUrl: string = (
|
||||
process.env["ACTIONS_CACHE_URL"] ||
|
||||
process.env["ACTIONS_RUNTIME_URL"] ||
|
||||
""
|
||||
).replace("pipelines", "artifactcache");
|
||||
if (!baseUrl) {
|
||||
throw new Error(
|
||||
"Cache Service Url not found, unable to restore cache."
|
||||
);
|
||||
}
|
||||
|
||||
const url = `${baseUrl}_apis/artifactcache/${resource}`;
|
||||
core.debug(`Resource Url: ${url}`);
|
||||
return url;
|
||||
}
|
||||
|
||||
function createAcceptHeader(type: string, apiVersion: string): string {
|
||||
return `${type};api-version=${apiVersion}`;
|
||||
}
|
||||
|
||||
function getRequestOptions(): IRequestOptions {
|
||||
const requestOptions: IRequestOptions = {
|
||||
headers: {
|
||||
Accept: createAcceptHeader("application/json", "6.0-preview.1")
|
||||
}
|
||||
};
|
||||
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
function createHttpClient(): HttpClient {
|
||||
const token = process.env["ACTIONS_RUNTIME_TOKEN"] || "";
|
||||
const bearerCredentialHandler = new BearerCredentialHandler(token);
|
||||
|
||||
return new HttpClient(
|
||||
"actions/cache",
|
||||
[bearerCredentialHandler],
|
||||
getRequestOptions()
|
||||
);
|
||||
}
|
||||
|
||||
export async function getCacheEntry(
|
||||
keys: string[]
|
||||
): Promise<ArtifactCacheEntry | null> {
|
||||
const httpClient = createHttpClient();
|
||||
const resource = `cache?keys=${encodeURIComponent(keys.join(","))}`;
|
||||
|
||||
const response = await httpClient.getJson<ArtifactCacheEntry>(
|
||||
getCacheApiUrl(resource)
|
||||
);
|
||||
if (response.statusCode === 204) {
|
||||
return null;
|
||||
}
|
||||
if (!isSuccessStatusCode(response.statusCode)) {
|
||||
throw new Error(`Cache service responded with ${response.statusCode}`);
|
||||
}
|
||||
|
||||
const cacheResult = response.result;
|
||||
const cacheDownloadUrl = cacheResult?.archiveLocation;
|
||||
if (!cacheDownloadUrl) {
|
||||
throw new Error("Cache not found.");
|
||||
}
|
||||
core.setSecret(cacheDownloadUrl);
|
||||
core.debug(`Cache Result:`);
|
||||
core.debug(JSON.stringify(cacheResult));
|
||||
|
||||
return cacheResult;
|
||||
}
|
||||
|
||||
async function pipeResponseToStream(
|
||||
response: IHttpClientResponse,
|
||||
stream: NodeJS.WritableStream
|
||||
): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
response.message.pipe(stream).on("close", () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export async function downloadCache(
|
||||
archiveLocation: string,
|
||||
archivePath: string
|
||||
): Promise<void> {
|
||||
const stream = fs.createWriteStream(archivePath);
|
||||
const httpClient = new HttpClient("actions/cache");
|
||||
const downloadResponse = await httpClient.get(archiveLocation);
|
||||
await pipeResponseToStream(downloadResponse, stream);
|
||||
}
|
||||
|
||||
// Reserve Cache
|
||||
export async function reserveCache(key: string): Promise<number> {
|
||||
const httpClient = createHttpClient();
|
||||
|
||||
const reserveCacheRequest: ReserveCacheRequest = {
|
||||
key
|
||||
};
|
||||
const response = await httpClient.postJson<ReserveCacheResponse>(
|
||||
getCacheApiUrl("caches"),
|
||||
reserveCacheRequest
|
||||
);
|
||||
return response?.result?.cacheId ?? -1;
|
||||
}
|
||||
|
||||
function getContentRange(start: number, end: number): string {
|
||||
// Format: `bytes start-end/filesize
|
||||
// start and end are inclusive
|
||||
// filesize can be *
|
||||
// For a 200 byte chunk starting at byte 0:
|
||||
// Content-Range: bytes 0-199/*
|
||||
return `bytes ${start}-${end}/*`;
|
||||
}
|
||||
|
||||
async function uploadChunk(
|
||||
httpClient: HttpClient,
|
||||
resourceUrl: string,
|
||||
data: NodeJS.ReadableStream,
|
||||
start: number,
|
||||
end: number
|
||||
): Promise<void> {
|
||||
core.debug(
|
||||
`Uploading chunk of size ${end -
|
||||
start +
|
||||
1} bytes at offset ${start} with content range: ${getContentRange(
|
||||
start,
|
||||
end
|
||||
)}`
|
||||
);
|
||||
const additionalHeaders = {
|
||||
"Content-Type": "application/octet-stream",
|
||||
"Content-Range": getContentRange(start, end)
|
||||
};
|
||||
|
||||
const uploadChunkRequest = async (): Promise<IHttpClientResponse> => {
|
||||
return await httpClient.sendStream(
|
||||
"PATCH",
|
||||
resourceUrl,
|
||||
data,
|
||||
additionalHeaders
|
||||
);
|
||||
};
|
||||
|
||||
const response = await uploadChunkRequest();
|
||||
if (isSuccessStatusCode(response.message.statusCode)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRetryableStatusCode(response.message.statusCode)) {
|
||||
core.debug(
|
||||
`Received ${response.message.statusCode}, retrying chunk at offset ${start}.`
|
||||
);
|
||||
const retryResponse = await uploadChunkRequest();
|
||||
if (isSuccessStatusCode(retryResponse.message.statusCode)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Cache service responded with ${response.message.statusCode} during chunk upload.`
|
||||
);
|
||||
}
|
||||
|
||||
function parseEnvNumber(key: string): number | undefined {
|
||||
const value = Number(process.env[key]);
|
||||
if (Number.isNaN(value) || value < 0) {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
async function uploadFile(
|
||||
httpClient: HttpClient,
|
||||
cacheId: number,
|
||||
archivePath: string
|
||||
): Promise<void> {
|
||||
// Upload Chunks
|
||||
const fileSize = fs.statSync(archivePath).size;
|
||||
const resourceUrl = getCacheApiUrl(`caches/${cacheId.toString()}`);
|
||||
const fd = fs.openSync(archivePath, "r");
|
||||
|
||||
const concurrency = parseEnvNumber("CACHE_UPLOAD_CONCURRENCY") ?? 4; // # of HTTP requests in parallel
|
||||
const MAX_CHUNK_SIZE =
|
||||
parseEnvNumber("CACHE_UPLOAD_CHUNK_SIZE") ?? 32 * 1024 * 1024; // 32 MB Chunks
|
||||
core.debug(`Concurrency: ${concurrency} and Chunk Size: ${MAX_CHUNK_SIZE}`);
|
||||
|
||||
const parallelUploads = [...new Array(concurrency).keys()];
|
||||
core.debug("Awaiting all uploads");
|
||||
let offset = 0;
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
parallelUploads.map(async () => {
|
||||
while (offset < fileSize) {
|
||||
const chunkSize = Math.min(
|
||||
fileSize - offset,
|
||||
MAX_CHUNK_SIZE
|
||||
);
|
||||
const start = offset;
|
||||
const end = offset + chunkSize - 1;
|
||||
offset += MAX_CHUNK_SIZE;
|
||||
const chunk = fs.createReadStream(archivePath, {
|
||||
fd,
|
||||
start,
|
||||
end,
|
||||
autoClose: false
|
||||
});
|
||||
|
||||
await uploadChunk(
|
||||
httpClient,
|
||||
resourceUrl,
|
||||
chunk,
|
||||
start,
|
||||
end
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async function commitCache(
|
||||
httpClient: HttpClient,
|
||||
cacheId: number,
|
||||
filesize: number
|
||||
): Promise<ITypedResponse<null>> {
|
||||
const commitCacheRequest: CommitCacheRequest = { size: filesize };
|
||||
return await httpClient.postJson<null>(
|
||||
getCacheApiUrl(`caches/${cacheId.toString()}`),
|
||||
commitCacheRequest
|
||||
);
|
||||
}
|
||||
|
||||
export async function saveCache(
|
||||
cacheId: number,
|
||||
archivePath: string
|
||||
): Promise<void> {
|
||||
const httpClient = createHttpClient();
|
||||
|
||||
core.debug("Upload cache");
|
||||
await uploadFile(httpClient, cacheId, archivePath);
|
||||
|
||||
// Commit Cache
|
||||
core.debug("Commiting cache");
|
||||
const cacheSize = utils.getArchiveFileSize(archivePath);
|
||||
const commitCacheResponse = await commitCache(
|
||||
httpClient,
|
||||
cacheId,
|
||||
cacheSize
|
||||
);
|
||||
if (!isSuccessStatusCode(commitCacheResponse.statusCode)) {
|
||||
throw new Error(
|
||||
`Cache service responded with ${commitCacheResponse.statusCode} during commit cache.`
|
||||
);
|
||||
}
|
||||
|
||||
core.info("Cache saved successfully");
|
||||
}
|
@ -9,8 +9,8 @@ export enum Outputs {
|
||||
}
|
||||
|
||||
export enum State {
|
||||
CacheKey = "CACHE_KEY",
|
||||
CacheResult = "CACHE_RESULT"
|
||||
CachePrimaryKey = "CACHE_KEY",
|
||||
CacheMatchedKey = "CACHE_RESULT"
|
||||
}
|
||||
|
||||
export enum Events {
|
||||
@ -18,3 +18,5 @@ export enum Events {
|
||||
Push = "push",
|
||||
PullRequest = "pull_request"
|
||||
}
|
||||
|
||||
export const RefKey = "GITHUB_REF";
|
||||
|
19
src/contracts.d.ts
vendored
19
src/contracts.d.ts
vendored
@ -1,19 +0,0 @@
|
||||
export interface ArtifactCacheEntry {
|
||||
cacheKey?: string;
|
||||
scope?: string;
|
||||
creationTime?: string;
|
||||
archiveLocation?: string;
|
||||
}
|
||||
|
||||
export interface CommitCacheRequest {
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface ReserveCacheRequest {
|
||||
key: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export interface ReserveCacheResponse {
|
||||
cacheId: number;
|
||||
}
|
105
src/restore.ts
105
src/restore.ts
@ -1,8 +1,7 @@
|
||||
import * as cache from "@actions/cache";
|
||||
import * as core from "@actions/core";
|
||||
import * as path from "path";
|
||||
import * as cacheHttpClient from "./cacheHttpClient";
|
||||
|
||||
import { Events, Inputs, State } from "./constants";
|
||||
import { extractTar } from "./tar";
|
||||
import * as utils from "./utils/actionUtils";
|
||||
|
||||
async function run(): Promise<void> {
|
||||
@ -12,97 +11,49 @@ async function run(): Promise<void> {
|
||||
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.`
|
||||
} is not supported because it's not tied to a branch or tag ref.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const cachePath = utils.resolvePath(
|
||||
core.getInput(Inputs.Path, { required: true })
|
||||
);
|
||||
core.debug(`Cache Path: ${cachePath}`);
|
||||
|
||||
const primaryKey = core.getInput(Inputs.Key, { required: true });
|
||||
core.saveState(State.CacheKey, primaryKey);
|
||||
core.saveState(State.CachePrimaryKey, primaryKey);
|
||||
|
||||
const restoreKeys = core
|
||||
.getInput(Inputs.RestoreKeys)
|
||||
.split("\n")
|
||||
.filter(x => x !== "");
|
||||
const keys = [primaryKey, ...restoreKeys];
|
||||
|
||||
core.debug("Resolved Keys:");
|
||||
core.debug(JSON.stringify(keys));
|
||||
|
||||
if (keys.length > 10) {
|
||||
core.setFailed(
|
||||
`Key Validation Error: Keys are limited to a maximum of 10.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
for (const key of keys) {
|
||||
if (key.length > 512) {
|
||||
core.setFailed(
|
||||
`Key Validation Error: ${key} cannot be larger than 512 characters.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
const regex = /^[^,]*$/;
|
||||
if (!regex.test(key)) {
|
||||
core.setFailed(
|
||||
`Key Validation Error: ${key} cannot contain commas.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
const restoreKeys = utils.getInputAsArray(Inputs.RestoreKeys);
|
||||
const cachePaths = utils.getInputAsArray(Inputs.Path, {
|
||||
required: true
|
||||
});
|
||||
|
||||
try {
|
||||
const cacheEntry = await cacheHttpClient.getCacheEntry(keys);
|
||||
if (!cacheEntry?.archiveLocation) {
|
||||
const cacheKey = await cache.restoreCache(
|
||||
cachePaths,
|
||||
primaryKey,
|
||||
restoreKeys
|
||||
);
|
||||
if (!cacheKey) {
|
||||
core.info(
|
||||
`Cache not found for input keys: ${keys.join(", ")}.`
|
||||
`Cache not found for input keys: ${[
|
||||
primaryKey,
|
||||
...restoreKeys
|
||||
].join(", ")}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const archivePath = path.join(
|
||||
await utils.createTempDirectory(),
|
||||
"cache.tgz"
|
||||
);
|
||||
core.debug(`Archive Path: ${archivePath}`);
|
||||
// Store the matched cache key
|
||||
utils.setCacheState(cacheKey);
|
||||
|
||||
// Store the cache result
|
||||
utils.setCacheState(cacheEntry);
|
||||
|
||||
// Download the cache from the cache entry
|
||||
await cacheHttpClient.downloadCache(
|
||||
cacheEntry.archiveLocation,
|
||||
archivePath
|
||||
);
|
||||
|
||||
const archiveFileSize = utils.getArchiveFileSize(archivePath);
|
||||
core.info(
|
||||
`Cache Size: ~${Math.round(
|
||||
archiveFileSize / (1024 * 1024)
|
||||
)} MB (${archiveFileSize} B)`
|
||||
);
|
||||
|
||||
await extractTar(archivePath, cachePath);
|
||||
|
||||
const isExactKeyMatch = utils.isExactKeyMatch(
|
||||
primaryKey,
|
||||
cacheEntry
|
||||
);
|
||||
const isExactKeyMatch = utils.isExactKeyMatch(primaryKey, cacheKey);
|
||||
utils.setCacheHitOutput(isExactKeyMatch);
|
||||
|
||||
core.info(
|
||||
`Cache restored from key: ${cacheEntry && cacheEntry.cacheKey}`
|
||||
);
|
||||
core.info(`Cache restored from key: ${cacheKey}`);
|
||||
} catch (error) {
|
||||
utils.logWarning(error.message);
|
||||
utils.setCacheHitOutput(false);
|
||||
if (error.name === cache.ValidationError.name) {
|
||||
throw error;
|
||||
} else {
|
||||
utils.logWarning(error.message);
|
||||
utils.setCacheHitOutput(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
core.setFailed(error.message);
|
||||
|
60
src/save.ts
60
src/save.ts
@ -1,8 +1,7 @@
|
||||
import * as cache from "@actions/cache";
|
||||
import * as core from "@actions/core";
|
||||
import * as path from "path";
|
||||
import * as cacheHttpClient from "./cacheHttpClient";
|
||||
|
||||
import { Events, Inputs, State } from "./constants";
|
||||
import { createTar } from "./tar";
|
||||
import * as utils from "./utils/actionUtils";
|
||||
|
||||
async function run(): Promise<void> {
|
||||
@ -11,9 +10,7 @@ async function run(): Promise<void> {
|
||||
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.`
|
||||
} is not supported because it's not tied to a branch or tag ref.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
@ -21,7 +18,7 @@ async function run(): Promise<void> {
|
||||
const state = utils.getCacheState();
|
||||
|
||||
// Inputs are re-evaluted before the post action, so we want the original key used for restore
|
||||
const primaryKey = core.getState(State.CacheKey);
|
||||
const primaryKey = core.getState(State.CachePrimaryKey);
|
||||
if (!primaryKey) {
|
||||
utils.logWarning(`Error retrieving key from state.`);
|
||||
return;
|
||||
@ -34,42 +31,21 @@ async function run(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
core.debug("Reserving Cache");
|
||||
const cacheId = await cacheHttpClient.reserveCache(primaryKey);
|
||||
if (cacheId == -1) {
|
||||
core.info(
|
||||
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
|
||||
);
|
||||
return;
|
||||
const cachePaths = utils.getInputAsArray(Inputs.Path, {
|
||||
required: true
|
||||
});
|
||||
|
||||
try {
|
||||
await cache.saveCache(cachePaths, primaryKey);
|
||||
} catch (error) {
|
||||
if (error.name === cache.ValidationError.name) {
|
||||
throw error;
|
||||
} else if (error.name === cache.ReserveCacheError.name) {
|
||||
core.info(error.message);
|
||||
} else {
|
||||
utils.logWarning(error.message);
|
||||
}
|
||||
}
|
||||
core.debug(`Cache ID: ${cacheId}`);
|
||||
const cachePath = utils.resolvePath(
|
||||
core.getInput(Inputs.Path, { required: true })
|
||||
);
|
||||
core.debug(`Cache Path: ${cachePath}`);
|
||||
|
||||
const archivePath = path.join(
|
||||
await utils.createTempDirectory(),
|
||||
"cache.tgz"
|
||||
);
|
||||
core.debug(`Archive Path: ${archivePath}`);
|
||||
|
||||
await createTar(archivePath, cachePath);
|
||||
|
||||
const fileSizeLimit = 5 * 1024 * 1024 * 1024; // 5GB per repo limit
|
||||
const archiveFileSize = utils.getArchiveFileSize(archivePath);
|
||||
core.debug(`File Size: ${archiveFileSize}`);
|
||||
if (archiveFileSize > fileSizeLimit) {
|
||||
utils.logWarning(
|
||||
`Cache size of ~${Math.round(
|
||||
archiveFileSize / (1024 * 1024)
|
||||
)} MB (${archiveFileSize} B) is over the 5GB limit, not saving cache.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
core.debug(`Saving Cache (ID: ${cacheId})`);
|
||||
await cacheHttpClient.saveCache(cacheId, archivePath);
|
||||
} catch (error) {
|
||||
utils.logWarning(error.message);
|
||||
}
|
||||
|
47
src/tar.ts
47
src/tar.ts
@ -1,47 +0,0 @@
|
||||
import { exec } from "@actions/exec";
|
||||
import * as io from "@actions/io";
|
||||
import { existsSync } from "fs";
|
||||
|
||||
async function getTarPath(): Promise<string> {
|
||||
// Explicitly use BSD Tar on Windows
|
||||
const IS_WINDOWS = process.platform === "win32";
|
||||
if (IS_WINDOWS) {
|
||||
const systemTar = `${process.env["windir"]}\\System32\\tar.exe`;
|
||||
if (existsSync(systemTar)) {
|
||||
return systemTar;
|
||||
}
|
||||
}
|
||||
return await io.which("tar", true);
|
||||
}
|
||||
|
||||
async function execTar(args: string[]): Promise<void> {
|
||||
try {
|
||||
await exec(`"${await getTarPath()}"`, args);
|
||||
} catch (error) {
|
||||
const IS_WINDOWS = process.platform === "win32";
|
||||
if (IS_WINDOWS) {
|
||||
throw new Error(
|
||||
`Tar failed with error: ${error?.message}. Ensure BSD tar is installed and on the PATH.`
|
||||
);
|
||||
}
|
||||
throw new Error(`Tar failed with error: ${error?.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function extractTar(
|
||||
archivePath: string,
|
||||
targetDirectory: string
|
||||
): Promise<void> {
|
||||
// Create directory to extract tar into
|
||||
await io.mkdirP(targetDirectory);
|
||||
const args = ["-xz", "-f", archivePath, "-C", targetDirectory];
|
||||
await execTar(args);
|
||||
}
|
||||
|
||||
export async function createTar(
|
||||
archivePath: string,
|
||||
sourceDirectory: string
|
||||
): Promise<void> {
|
||||
const args = ["-cz", "-f", archivePath, "-C", sourceDirectory, "."];
|
||||
await execTar(args);
|
||||
}
|
@ -1,77 +1,35 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as io from "@actions/io";
|
||||
import * as fs from "fs";
|
||||
import * as os from "os";
|
||||
import * as path from "path";
|
||||
import * as uuidV4 from "uuid/v4";
|
||||
|
||||
import { Events, Outputs, State } from "../constants";
|
||||
import { ArtifactCacheEntry } from "../contracts";
|
||||
import { Outputs, RefKey, State } from "../constants";
|
||||
|
||||
// From https://github.com/actions/toolkit/blob/master/packages/tool-cache/src/tool-cache.ts#L23
|
||||
export async function createTempDirectory(): Promise<string> {
|
||||
const IS_WINDOWS = process.platform === "win32";
|
||||
|
||||
let tempDirectory: string = process.env["RUNNER_TEMP"] || "";
|
||||
|
||||
if (!tempDirectory) {
|
||||
let baseLocation: string;
|
||||
if (IS_WINDOWS) {
|
||||
// On Windows use the USERPROFILE env variable
|
||||
baseLocation = process.env["USERPROFILE"] || "C:\\";
|
||||
} else {
|
||||
if (process.platform === "darwin") {
|
||||
baseLocation = "/Users";
|
||||
} else {
|
||||
baseLocation = "/home";
|
||||
}
|
||||
}
|
||||
tempDirectory = path.join(baseLocation, "actions", "temp");
|
||||
}
|
||||
const dest = path.join(tempDirectory, uuidV4.default());
|
||||
await io.mkdirP(dest);
|
||||
return dest;
|
||||
}
|
||||
|
||||
export function getArchiveFileSize(path: string): number {
|
||||
return fs.statSync(path).size;
|
||||
}
|
||||
|
||||
export function isExactKeyMatch(
|
||||
key: string,
|
||||
cacheResult?: ArtifactCacheEntry
|
||||
): boolean {
|
||||
export function isExactKeyMatch(key: string, cacheKey?: string): boolean {
|
||||
return !!(
|
||||
cacheResult &&
|
||||
cacheResult.cacheKey &&
|
||||
cacheResult.cacheKey.localeCompare(key, undefined, {
|
||||
cacheKey &&
|
||||
cacheKey.localeCompare(key, undefined, {
|
||||
sensitivity: "accent"
|
||||
}) === 0
|
||||
);
|
||||
}
|
||||
|
||||
export function setCacheState(state: ArtifactCacheEntry): void {
|
||||
core.saveState(State.CacheResult, JSON.stringify(state));
|
||||
export function setCacheState(state: string): void {
|
||||
core.saveState(State.CacheMatchedKey, state);
|
||||
}
|
||||
|
||||
export function setCacheHitOutput(isCacheHit: boolean): void {
|
||||
core.setOutput(Outputs.CacheHit, isCacheHit.toString());
|
||||
}
|
||||
|
||||
export function setOutputAndState(
|
||||
key: string,
|
||||
cacheResult?: ArtifactCacheEntry
|
||||
): void {
|
||||
setCacheHitOutput(isExactKeyMatch(key, cacheResult));
|
||||
// Store the cache result if it exists
|
||||
cacheResult && setCacheState(cacheResult);
|
||||
export function setOutputAndState(key: string, cacheKey?: string): void {
|
||||
setCacheHitOutput(isExactKeyMatch(key, cacheKey));
|
||||
// Store the matched cache key if it exists
|
||||
cacheKey && setCacheState(cacheKey);
|
||||
}
|
||||
|
||||
export function getCacheState(): ArtifactCacheEntry | undefined {
|
||||
const stateData = core.getState(State.CacheResult);
|
||||
core.debug(`State: ${stateData}`);
|
||||
if (stateData) {
|
||||
return JSON.parse(stateData) as ArtifactCacheEntry;
|
||||
export function getCacheState(): string | undefined {
|
||||
const cacheKey = core.getState(State.CacheMatchedKey);
|
||||
if (cacheKey) {
|
||||
core.debug(`Cache state/key: ${cacheKey}`);
|
||||
return cacheKey;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
@ -82,26 +40,19 @@ export function logWarning(message: string): void {
|
||||
core.info(`${warningPrefix}${message}`);
|
||||
}
|
||||
|
||||
export function resolvePath(filePath: string): string {
|
||||
if (filePath[0] === "~") {
|
||||
const home = os.homedir();
|
||||
if (!home) {
|
||||
throw new Error("Unable to resolve `~` to HOME");
|
||||
}
|
||||
return path.join(home, filePath.slice(1));
|
||||
}
|
||||
|
||||
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
|
||||
// Cache token authorized for all events that are tied to a ref
|
||||
// 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);
|
||||
return RefKey in process.env && Boolean(process.env[RefKey]);
|
||||
}
|
||||
|
||||
export function getInputAsArray(
|
||||
name: string,
|
||||
options?: core.InputOptions
|
||||
): string[] {
|
||||
return core
|
||||
.getInput(name, options)
|
||||
.split("\n")
|
||||
.map(s => s.trim())
|
||||
.filter(x => x !== "");
|
||||
}
|
||||
|
Reference in New Issue
Block a user