mirror of
https://github.com/actions/cache.git
synced 2025-06-24 19:31:10 +02:00
Compare commits
76 Commits
Author | SHA1 | Date | |
---|---|---|---|
f5ce41475b | |||
68fa0a8d81 | |||
56ec64e417 | |||
efbc4e162b | |||
d9747005de | |||
3f662ca624 | |||
0232e3178d | |||
ee7a57c615 | |||
da9f90cb83 | |||
ec7f7ebd08 | |||
2a973a0f4e | |||
cbbb8b4d4f | |||
5a0add1806 | |||
9fe7ad8b07 | |||
7c7d003bbb | |||
96e5a46c57 | |||
84e606dfac | |||
70655ec832 | |||
78a4b2143b | |||
4dc4b4e758 | |||
85aee6a487 | |||
fe1055e9d1 | |||
fab26f3f4f | |||
4887979af8 | |||
f9c9166ecb | |||
23e301d35c | |||
e43776276f | |||
b6d538e2aa | |||
296374f6c9 | |||
6c11532937 | |||
c33bff8d72 | |||
d1991bb4c5 | |||
60e292adf7 | |||
a505c2e7a6 | |||
c262ac0154 | |||
10a14413e7 | |||
cf4f44db70 | |||
4c4974aff1 | |||
1da52de10f | |||
b45d91cc4b | |||
a631fadf14 | |||
e223b0a12d | |||
decbafc350 | |||
3854a40aee | |||
0188dffc5a | |||
002d3a77f4 | |||
4809f4ada4 | |||
3d01b4eb53 | |||
cffae9552b | |||
95c1798369 | |||
639f9d8b81 | |||
d9fe1b81f9 | |||
92ae3b63f8 | |||
84b3b283f0 | |||
8d14a2150b | |||
c0584c42d1 | |||
bb828da54c | |||
7e7aef2963 | |||
b7d83b4095 | |||
50a2fdee6f | |||
f0cbadd748 | |||
4657a5f525 | |||
fb50aa45ec | |||
31508256ff | |||
bc821d0c12 | |||
bde557aefd | |||
4b0709a0d5 | |||
ecf6eea708 | |||
eb10706a9d | |||
30524a6fbd | |||
b034b26a44 | |||
e1ed41a9c9 | |||
5f4d4d4555 | |||
5d3ad75a2b | |||
d8c5e69fe2 | |||
f66a56e59e |
112
.github/workflows/workflow.yml
vendored
112
.github/workflows/workflow.yml
vendored
@ -4,50 +4,130 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- releases/**
|
||||
paths-ignore:
|
||||
- '**.md'
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- 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]
|
||||
|
||||
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@v1
|
||||
with:
|
||||
path: ${{ steps.npm-cache.outputs.dir }}
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- run: npm ci
|
||||
|
||||
- name: Prettier Format Check
|
||||
run: npm run format-check
|
||||
|
||||
- name: ESLint Check
|
||||
run: npm run lint
|
||||
|
||||
- name: Build & Test
|
||||
run: npm run test
|
||||
|
||||
# End to end save and restore
|
||||
test-save:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Generate files
|
||||
shell: bash
|
||||
run: __tests__/create-cache-files.sh ${{ runner.os }}
|
||||
- name: Save cache
|
||||
uses: ./
|
||||
with:
|
||||
key: test-${{ runner.os }}-${{ github.run_id }}
|
||||
path: test-cache
|
||||
test-restore:
|
||||
needs: test-save
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, 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
|
||||
- name: Verify cache
|
||||
shell: bash
|
||||
run: __tests__/verify-cache-files.sh ${{ runner.os }}
|
||||
|
||||
# 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: ubuntu/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
|
||||
- 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: ubuntu/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
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -94,3 +94,6 @@ typings/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# Text editor files
|
||||
.vscode/
|
||||
|
36
README.md
36
README.md
@ -1,8 +1,8 @@
|
||||
# cache
|
||||
|
||||
This GitHub Action allows caching dependencies and build outputs to improve workflow execution time.
|
||||
This action allows caching dependencies and build outputs to improve workflow execution time.
|
||||
|
||||
<a href="https://github.com/actions/cache"><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=master&event=push"></a>
|
||||
|
||||
## Documentation
|
||||
|
||||
@ -35,7 +35,7 @@ on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
@ -49,18 +49,38 @@ jobs:
|
||||
- name: Generate Prime Numbers
|
||||
if: steps.cache-primes.outputs.cache-hit != 'true'
|
||||
run: /generate-primes.sh -d prime-numbers
|
||||
|
||||
|
||||
- name: Use Prime Numbers
|
||||
run: /primes.sh -d prime-numbers
|
||||
```
|
||||
|
||||
## Ecosystem Examples
|
||||
## Implementation Examples
|
||||
|
||||
See [Examples](examples.md)
|
||||
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)
|
||||
- [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 - Yarn](./examples.md#node---yarn)
|
||||
- [PHP - Composer](./examples.md#php---composer)
|
||||
- [Python - pip](./examples.md#python---pip)
|
||||
- [R - renv](./examples.md#r---renv)
|
||||
- [Ruby - Bundler](./examples.md#ruby---bundler)
|
||||
- [Rust - Cargo](./examples.md#rust---cargo)
|
||||
- [Scala - SBT](./examples.md#scala---sbt)
|
||||
- [Swift, Objective-C - Carthage](./examples.md#swift-objective-c---carthage)
|
||||
- [Swift, Objective-C - CocoaPods](./examples.md#swift-objective-c---cocoapods)
|
||||
- [Swift - Swift Package Manager](./examples.md#swift---swift-package-manager)
|
||||
|
||||
## Cache Limits
|
||||
|
||||
Individual caches are limited to 400MB and a repository can have up to 2GB of caches. Once the 2GB limit is reached, older caches will be evicted based on when the cache was last accessed. Caches that are not accessed within the last week will also be evicted.
|
||||
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.
|
||||
|
||||
## Skipping steps based on cache-hit
|
||||
|
||||
@ -76,7 +96,7 @@ steps:
|
||||
with:
|
||||
path: path/to/dependencies
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
|
||||
|
||||
|
||||
- name: Install Dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: /install.sh
|
||||
|
@ -162,6 +162,16 @@ test("getCacheState with valid state", () => {
|
||||
expect(getStateMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("logWarning logs a message with a warning prefix", () => {
|
||||
const message = "A warning occurred.";
|
||||
|
||||
const infoMock = jest.spyOn(core, "info");
|
||||
|
||||
actionUtils.logWarning(message);
|
||||
|
||||
expect(infoMock).toHaveBeenCalledWith(`[warning]${message}`);
|
||||
});
|
||||
|
||||
test("isValidEvent returns false for unknown event", () => {
|
||||
const event = "foo";
|
||||
process.env[Events.Key] = event;
|
||||
|
144
__tests__/cacheHttpsClient.test.ts
Normal file
144
__tests__/cacheHttpsClient.test.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import { retry } from "../src/cacheHttpClient";
|
||||
import * as testUtils from "../src/utils/testUtils";
|
||||
|
||||
afterEach(() => {
|
||||
testUtils.clearInputs();
|
||||
});
|
||||
|
||||
interface TestResponse {
|
||||
statusCode: number;
|
||||
result: string | null;
|
||||
}
|
||||
|
||||
function handleResponse(
|
||||
response: TestResponse | undefined
|
||||
): Promise<TestResponse> {
|
||||
if (!response) {
|
||||
fail("Retry method called too many times");
|
||||
}
|
||||
|
||||
if (response.statusCode === 999) {
|
||||
throw Error("Test Error");
|
||||
} else {
|
||||
return Promise.resolve(response);
|
||||
}
|
||||
}
|
||||
|
||||
async function testRetryExpectingResult(
|
||||
responses: Array<TestResponse>,
|
||||
expectedResult: string | null
|
||||
): Promise<void> {
|
||||
responses = responses.reverse(); // Reverse responses since we pop from end
|
||||
|
||||
const actualResult = await retry(
|
||||
"test",
|
||||
() => handleResponse(responses.pop()),
|
||||
(response: TestResponse) => response.statusCode
|
||||
);
|
||||
|
||||
expect(actualResult.result).toEqual(expectedResult);
|
||||
}
|
||||
|
||||
async function testRetryExpectingError(
|
||||
responses: Array<TestResponse>
|
||||
): Promise<void> {
|
||||
responses = responses.reverse(); // Reverse responses since we pop from end
|
||||
|
||||
expect(
|
||||
retry(
|
||||
"test",
|
||||
() => handleResponse(responses.pop()),
|
||||
(response: TestResponse) => response.statusCode
|
||||
)
|
||||
).rejects.toBeInstanceOf(Error);
|
||||
}
|
||||
|
||||
test("retry works on successful response", async () => {
|
||||
await testRetryExpectingResult(
|
||||
[
|
||||
{
|
||||
statusCode: 200,
|
||||
result: "Ok"
|
||||
}
|
||||
],
|
||||
"Ok"
|
||||
);
|
||||
});
|
||||
|
||||
test("retry works after retryable status code", async () => {
|
||||
await testRetryExpectingResult(
|
||||
[
|
||||
{
|
||||
statusCode: 503,
|
||||
result: null
|
||||
},
|
||||
{
|
||||
statusCode: 200,
|
||||
result: "Ok"
|
||||
}
|
||||
],
|
||||
"Ok"
|
||||
);
|
||||
});
|
||||
|
||||
test("retry fails after exhausting retries", async () => {
|
||||
await testRetryExpectingError([
|
||||
{
|
||||
statusCode: 503,
|
||||
result: null
|
||||
},
|
||||
{
|
||||
statusCode: 503,
|
||||
result: null
|
||||
},
|
||||
{
|
||||
statusCode: 200,
|
||||
result: "Ok"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test("retry fails after non-retryable status code", async () => {
|
||||
await testRetryExpectingError([
|
||||
{
|
||||
statusCode: 500,
|
||||
result: null
|
||||
},
|
||||
{
|
||||
statusCode: 200,
|
||||
result: "Ok"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test("retry works after error", async () => {
|
||||
await testRetryExpectingResult(
|
||||
[
|
||||
{
|
||||
statusCode: 999,
|
||||
result: null
|
||||
},
|
||||
{
|
||||
statusCode: 200,
|
||||
result: "Ok"
|
||||
}
|
||||
],
|
||||
"Ok"
|
||||
);
|
||||
});
|
||||
|
||||
test("retry returns after client error", async () => {
|
||||
await testRetryExpectingResult(
|
||||
[
|
||||
{
|
||||
statusCode: 400,
|
||||
result: null
|
||||
},
|
||||
{
|
||||
statusCode: 200,
|
||||
result: "Ok"
|
||||
}
|
||||
],
|
||||
null
|
||||
);
|
||||
});
|
11
__tests__/create-cache-files.sh
Executable file
11
__tests__/create-cache-files.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Validate args
|
||||
prefix="$1"
|
||||
if [ -z "$prefix" ]; then
|
||||
echo "Must supply prefix argument"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir test-cache
|
||||
echo "$prefix $GITHUB_RUN_ID" > test-cache/test-file.txt
|
@ -1,18 +1,16 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as exec from "@actions/exec";
|
||||
import * as io from "@actions/io";
|
||||
import * as path from "path";
|
||||
import * as cacheHttpClient from "../src/cacheHttpClient";
|
||||
import { Events, Inputs } from "../src/constants";
|
||||
import { ArtifactCacheEntry } from "../src/contracts";
|
||||
import run from "../src/restore";
|
||||
import * as tar from "../src/tar";
|
||||
import * as actionUtils from "../src/utils/actionUtils";
|
||||
import * as testUtils from "../src/utils/testUtils";
|
||||
|
||||
jest.mock("@actions/exec");
|
||||
jest.mock("@actions/io");
|
||||
jest.mock("../src/utils/actionUtils");
|
||||
jest.mock("../src/cacheHttpClient");
|
||||
jest.mock("../src/tar");
|
||||
jest.mock("../src/utils/actionUtils");
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(actionUtils, "resolvePath").mockImplementation(filePath => {
|
||||
@ -35,10 +33,6 @@ beforeAll(() => {
|
||||
const actualUtils = jest.requireActual("../src/utils/actionUtils");
|
||||
return actualUtils.getSupportedEvents();
|
||||
});
|
||||
|
||||
jest.spyOn(io, "which").mockImplementation(tool => {
|
||||
return Promise.resolve(tool);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@ -50,14 +44,16 @@ afterEach(() => {
|
||||
delete process.env[Events.Key];
|
||||
});
|
||||
|
||||
test("restore with invalid event", async () => {
|
||||
test("restore with invalid event outputs warning", async () => {
|
||||
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
const invalidEvent = "commit_comment";
|
||||
process.env[Events.Key] = invalidEvent;
|
||||
await run();
|
||||
expect(failedMock).toHaveBeenCalledWith(
|
||||
expect(logWarningMock).toHaveBeenCalledWith(
|
||||
`Event Validation Error: The event type ${invalidEvent} is not supported. Only push, pull_request events are supported at this time.`
|
||||
);
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test("restore with no path should fail", async () => {
|
||||
@ -126,7 +122,6 @@ test("restore with no cache found", async () => {
|
||||
});
|
||||
|
||||
const infoMock = jest.spyOn(core, "info");
|
||||
const warningMock = jest.spyOn(core, "warning");
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
const stateMock = jest.spyOn(core, "saveState");
|
||||
|
||||
@ -138,7 +133,6 @@ test("restore with no cache found", async () => {
|
||||
await run();
|
||||
|
||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||
expect(warningMock).toHaveBeenCalledTimes(0);
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(infoMock).toHaveBeenCalledWith(
|
||||
@ -153,7 +147,7 @@ test("restore with server error should fail", async () => {
|
||||
key
|
||||
});
|
||||
|
||||
const warningMock = jest.spyOn(core, "warning");
|
||||
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
const stateMock = jest.spyOn(core, "saveState");
|
||||
|
||||
@ -168,8 +162,8 @@ test("restore with server error should fail", async () => {
|
||||
|
||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||
|
||||
expect(warningMock).toHaveBeenCalledTimes(1);
|
||||
expect(warningMock).toHaveBeenCalledWith("HTTP Error Occurred");
|
||||
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
||||
expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
|
||||
|
||||
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
||||
expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
|
||||
@ -187,7 +181,6 @@ test("restore with restore keys and no cache found", async () => {
|
||||
});
|
||||
|
||||
const infoMock = jest.spyOn(core, "info");
|
||||
const warningMock = jest.spyOn(core, "warning");
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
const stateMock = jest.spyOn(core, "saveState");
|
||||
|
||||
@ -199,7 +192,6 @@ test("restore with restore keys and no cache found", async () => {
|
||||
await run();
|
||||
|
||||
expect(stateMock).toHaveBeenCalledWith("CACHE_KEY", key);
|
||||
expect(warningMock).toHaveBeenCalledTimes(0);
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(infoMock).toHaveBeenCalledWith(
|
||||
@ -216,7 +208,6 @@ test("restore with cache found", async () => {
|
||||
});
|
||||
|
||||
const infoMock = jest.spyOn(core, "info");
|
||||
const warningMock = jest.spyOn(core, "warning");
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
const stateMock = jest.spyOn(core, "saveState");
|
||||
|
||||
@ -248,8 +239,7 @@ test("restore with cache found", async () => {
|
||||
.spyOn(actionUtils, "getArchiveFileSize")
|
||||
.mockReturnValue(fileSize);
|
||||
|
||||
const mkdirMock = jest.spyOn(io, "mkdirP");
|
||||
const execMock = jest.spyOn(exec, "exec");
|
||||
const extractTarMock = jest.spyOn(tar, "extractTar");
|
||||
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
|
||||
|
||||
await run();
|
||||
@ -258,30 +248,19 @@ test("restore with cache found", async () => {
|
||||
expect(getCacheMock).toHaveBeenCalledWith([key]);
|
||||
expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry);
|
||||
expect(createTempDirectoryMock).toHaveBeenCalledTimes(1);
|
||||
expect(downloadCacheMock).toHaveBeenCalledWith(cacheEntry, archivePath);
|
||||
expect(downloadCacheMock).toHaveBeenCalledWith(
|
||||
cacheEntry.archiveLocation,
|
||||
archivePath
|
||||
);
|
||||
expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath);
|
||||
expect(mkdirMock).toHaveBeenCalledWith(cachePath);
|
||||
|
||||
const IS_WINDOWS = process.platform === "win32";
|
||||
const args = IS_WINDOWS
|
||||
? [
|
||||
"-xz",
|
||||
"--force-local",
|
||||
"-f",
|
||||
archivePath.replace(/\\/g, "/"),
|
||||
"-C",
|
||||
cachePath.replace(/\\/g, "/")
|
||||
]
|
||||
: ["-xz", "-f", archivePath, "-C", cachePath];
|
||||
|
||||
expect(execMock).toHaveBeenCalledTimes(1);
|
||||
expect(execMock).toHaveBeenCalledWith(`"tar"`, args);
|
||||
expect(extractTarMock).toHaveBeenCalledTimes(1);
|
||||
expect(extractTarMock).toHaveBeenCalledWith(archivePath, cachePath);
|
||||
|
||||
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
||||
expect(setCacheHitOutputMock).toHaveBeenCalledWith(true);
|
||||
|
||||
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
|
||||
expect(warningMock).toHaveBeenCalledTimes(0);
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
@ -296,7 +275,6 @@ test("restore with a pull request event and cache found", async () => {
|
||||
process.env[Events.Key] = Events.PullRequest;
|
||||
|
||||
const infoMock = jest.spyOn(core, "info");
|
||||
const warningMock = jest.spyOn(core, "warning");
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
const stateMock = jest.spyOn(core, "saveState");
|
||||
|
||||
@ -328,8 +306,7 @@ test("restore with a pull request event and cache found", async () => {
|
||||
.spyOn(actionUtils, "getArchiveFileSize")
|
||||
.mockReturnValue(fileSize);
|
||||
|
||||
const mkdirMock = jest.spyOn(io, "mkdirP");
|
||||
const execMock = jest.spyOn(exec, "exec");
|
||||
const extractTarMock = jest.spyOn(tar, "extractTar");
|
||||
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
|
||||
|
||||
await run();
|
||||
@ -338,31 +315,20 @@ test("restore with a pull request event and cache found", async () => {
|
||||
expect(getCacheMock).toHaveBeenCalledWith([key]);
|
||||
expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry);
|
||||
expect(createTempDirectoryMock).toHaveBeenCalledTimes(1);
|
||||
expect(downloadCacheMock).toHaveBeenCalledWith(cacheEntry, archivePath);
|
||||
expect(downloadCacheMock).toHaveBeenCalledWith(
|
||||
cacheEntry.archiveLocation,
|
||||
archivePath
|
||||
);
|
||||
expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath);
|
||||
expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~60 MB (62915000 B)`);
|
||||
expect(mkdirMock).toHaveBeenCalledWith(cachePath);
|
||||
|
||||
const IS_WINDOWS = process.platform === "win32";
|
||||
const args = IS_WINDOWS
|
||||
? [
|
||||
"-xz",
|
||||
"--force-local",
|
||||
"-f",
|
||||
archivePath.replace(/\\/g, "/"),
|
||||
"-C",
|
||||
cachePath.replace(/\\/g, "/")
|
||||
]
|
||||
: ["-xz", "-f", archivePath, "-C", cachePath];
|
||||
|
||||
expect(execMock).toHaveBeenCalledTimes(1);
|
||||
expect(execMock).toHaveBeenCalledWith(`"tar"`, args);
|
||||
expect(extractTarMock).toHaveBeenCalledTimes(1);
|
||||
expect(extractTarMock).toHaveBeenCalledWith(archivePath, cachePath);
|
||||
|
||||
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
||||
expect(setCacheHitOutputMock).toHaveBeenCalledWith(true);
|
||||
|
||||
expect(infoMock).toHaveBeenCalledWith(`Cache restored from key: ${key}`);
|
||||
expect(warningMock).toHaveBeenCalledTimes(0);
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
@ -377,7 +343,6 @@ test("restore with cache found for restore key", async () => {
|
||||
});
|
||||
|
||||
const infoMock = jest.spyOn(core, "info");
|
||||
const warningMock = jest.spyOn(core, "warning");
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
const stateMock = jest.spyOn(core, "saveState");
|
||||
|
||||
@ -409,8 +374,7 @@ test("restore with cache found for restore key", async () => {
|
||||
.spyOn(actionUtils, "getArchiveFileSize")
|
||||
.mockReturnValue(fileSize);
|
||||
|
||||
const mkdirMock = jest.spyOn(io, "mkdirP");
|
||||
const execMock = jest.spyOn(exec, "exec");
|
||||
const extractTarMock = jest.spyOn(tar, "extractTar");
|
||||
const setCacheHitOutputMock = jest.spyOn(actionUtils, "setCacheHitOutput");
|
||||
|
||||
await run();
|
||||
@ -419,25 +383,15 @@ test("restore with cache found for restore key", async () => {
|
||||
expect(getCacheMock).toHaveBeenCalledWith([key, restoreKey]);
|
||||
expect(setCacheStateMock).toHaveBeenCalledWith(cacheEntry);
|
||||
expect(createTempDirectoryMock).toHaveBeenCalledTimes(1);
|
||||
expect(downloadCacheMock).toHaveBeenCalledWith(cacheEntry, archivePath);
|
||||
expect(downloadCacheMock).toHaveBeenCalledWith(
|
||||
cacheEntry.archiveLocation,
|
||||
archivePath
|
||||
);
|
||||
expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath);
|
||||
expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~0 MB (142 B)`);
|
||||
expect(mkdirMock).toHaveBeenCalledWith(cachePath);
|
||||
|
||||
const IS_WINDOWS = process.platform === "win32";
|
||||
const args = IS_WINDOWS
|
||||
? [
|
||||
"-xz",
|
||||
"--force-local",
|
||||
"-f",
|
||||
archivePath.replace(/\\/g, "/"),
|
||||
"-C",
|
||||
cachePath.replace(/\\/g, "/")
|
||||
]
|
||||
: ["-xz", "-f", archivePath, "-C", cachePath];
|
||||
|
||||
expect(execMock).toHaveBeenCalledTimes(1);
|
||||
expect(execMock).toHaveBeenCalledWith(`"tar"`, args);
|
||||
expect(extractTarMock).toHaveBeenCalledTimes(1);
|
||||
expect(extractTarMock).toHaveBeenCalledWith(archivePath, cachePath);
|
||||
|
||||
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
|
||||
expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);
|
||||
@ -445,6 +399,5 @@ test("restore with cache found for restore key", async () => {
|
||||
expect(infoMock).toHaveBeenCalledWith(
|
||||
`Cache restored from key: ${restoreKey}`
|
||||
);
|
||||
expect(warningMock).toHaveBeenCalledTimes(0);
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
@ -1,19 +1,17 @@
|
||||
import * as core from "@actions/core";
|
||||
import * as exec from "@actions/exec";
|
||||
import * as io from "@actions/io";
|
||||
import * as path from "path";
|
||||
import * as cacheHttpClient from "../src/cacheHttpClient";
|
||||
import { Inputs } from "../src/constants";
|
||||
import { Events, Inputs } from "../src/constants";
|
||||
import { ArtifactCacheEntry } from "../src/contracts";
|
||||
import run from "../src/save";
|
||||
import * as tar from "../src/tar";
|
||||
import * as actionUtils from "../src/utils/actionUtils";
|
||||
import * as testUtils from "../src/utils/testUtils";
|
||||
|
||||
jest.mock("@actions/core");
|
||||
jest.mock("@actions/exec");
|
||||
jest.mock("@actions/io");
|
||||
jest.mock("../src/utils/actionUtils");
|
||||
jest.mock("../src/cacheHttpClient");
|
||||
jest.mock("../src/tar");
|
||||
jest.mock("../src/utils/actionUtils");
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(core, "getInput").mockImplementation((name, options) => {
|
||||
@ -32,6 +30,16 @@ beforeAll(() => {
|
||||
}
|
||||
);
|
||||
|
||||
jest.spyOn(actionUtils, "isValidEvent").mockImplementation(() => {
|
||||
const actualUtils = jest.requireActual("../src/utils/actionUtils");
|
||||
return actualUtils.isValidEvent();
|
||||
});
|
||||
|
||||
jest.spyOn(actionUtils, "getSupportedEvents").mockImplementation(() => {
|
||||
const actualUtils = jest.requireActual("../src/utils/actionUtils");
|
||||
return actualUtils.getSupportedEvents();
|
||||
});
|
||||
|
||||
jest.spyOn(actionUtils, "resolvePath").mockImplementation(filePath => {
|
||||
return path.resolve(filePath);
|
||||
});
|
||||
@ -39,18 +47,31 @@ beforeAll(() => {
|
||||
jest.spyOn(actionUtils, "createTempDirectory").mockImplementation(() => {
|
||||
return Promise.resolve("/foo/bar");
|
||||
});
|
||||
});
|
||||
|
||||
jest.spyOn(io, "which").mockImplementation(tool => {
|
||||
return Promise.resolve(tool);
|
||||
});
|
||||
beforeEach(() => {
|
||||
process.env[Events.Key] = Events.Push;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
testUtils.clearInputs();
|
||||
delete process.env[Events.Key];
|
||||
});
|
||||
|
||||
test("save with invalid event outputs warning", async () => {
|
||||
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
const invalidEvent = "commit_comment";
|
||||
process.env[Events.Key] = invalidEvent;
|
||||
await run();
|
||||
expect(logWarningMock).toHaveBeenCalledWith(
|
||||
`Event Validation Error: The event type ${invalidEvent} is not supported. Only push, pull_request events are supported at this time.`
|
||||
);
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test("save with no primary key in state outputs warning", async () => {
|
||||
const warningMock = jest.spyOn(core, "warning");
|
||||
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
|
||||
const cacheEntry: ArtifactCacheEntry = {
|
||||
@ -72,16 +93,15 @@ test("save with no primary key in state outputs warning", async () => {
|
||||
|
||||
await run();
|
||||
|
||||
expect(warningMock).toHaveBeenCalledWith(
|
||||
expect(logWarningMock).toHaveBeenCalledWith(
|
||||
`Error retrieving key from state.`
|
||||
);
|
||||
expect(warningMock).toHaveBeenCalledTimes(1);
|
||||
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test("save with exact match returns early", async () => {
|
||||
const infoMock = jest.spyOn(core, "info");
|
||||
const warningMock = jest.spyOn(core, "warning");
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
|
||||
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
||||
@ -102,7 +122,7 @@ test("save with exact match returns early", async () => {
|
||||
return primaryKey;
|
||||
});
|
||||
|
||||
const execMock = jest.spyOn(exec, "exec");
|
||||
const createTarMock = jest.spyOn(tar, "createTar");
|
||||
|
||||
await run();
|
||||
|
||||
@ -110,14 +130,13 @@ test("save with exact match returns early", async () => {
|
||||
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
|
||||
);
|
||||
|
||||
expect(execMock).toHaveBeenCalledTimes(0);
|
||||
expect(createTarMock).toHaveBeenCalledTimes(0);
|
||||
|
||||
expect(warningMock).toHaveBeenCalledTimes(0);
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test("save with missing input outputs warning", async () => {
|
||||
const warningMock = jest.spyOn(core, "warning");
|
||||
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
|
||||
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
||||
@ -140,15 +159,15 @@ test("save with missing input outputs warning", async () => {
|
||||
|
||||
await run();
|
||||
|
||||
expect(warningMock).toHaveBeenCalledWith(
|
||||
expect(logWarningMock).toHaveBeenCalledWith(
|
||||
"Input required and not supplied: path"
|
||||
);
|
||||
expect(warningMock).toHaveBeenCalledTimes(1);
|
||||
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test("save with large cache outputs warning", async () => {
|
||||
const warningMock = jest.spyOn(core, "warning");
|
||||
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
|
||||
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
||||
@ -173,9 +192,9 @@ test("save with large cache outputs warning", async () => {
|
||||
const cachePath = path.resolve(inputPath);
|
||||
testUtils.setInput(Inputs.Path, inputPath);
|
||||
|
||||
const execMock = jest.spyOn(exec, "exec");
|
||||
const createTarMock = jest.spyOn(tar, "createTar");
|
||||
|
||||
const cacheSize = 1024 * 1024 * 1024; //~1GB, over the 400MB limit
|
||||
const cacheSize = 6 * 1024 * 1024 * 1024; //~6GB, over the 5GB limit
|
||||
jest.spyOn(actionUtils, "getArchiveFileSize").mockImplementationOnce(() => {
|
||||
return cacheSize;
|
||||
});
|
||||
@ -184,32 +203,70 @@ test("save with large cache outputs warning", async () => {
|
||||
|
||||
const archivePath = path.join("/foo/bar", "cache.tgz");
|
||||
|
||||
const IS_WINDOWS = process.platform === "win32";
|
||||
const args = IS_WINDOWS
|
||||
? [
|
||||
"-cz",
|
||||
"--force-local",
|
||||
"-f",
|
||||
archivePath.replace(/\\/g, "/"),
|
||||
"-C",
|
||||
cachePath.replace(/\\/g, "/"),
|
||||
"."
|
||||
]
|
||||
: ["-cz", "-f", archivePath, "-C", cachePath, "."];
|
||||
expect(createTarMock).toHaveBeenCalledTimes(1);
|
||||
expect(createTarMock).toHaveBeenCalledWith(archivePath, cachePath);
|
||||
|
||||
expect(execMock).toHaveBeenCalledTimes(1);
|
||||
expect(execMock).toHaveBeenCalledWith(`"tar"`, args);
|
||||
|
||||
expect(warningMock).toHaveBeenCalledTimes(1);
|
||||
expect(warningMock).toHaveBeenCalledWith(
|
||||
"Cache size of ~1024 MB (1073741824 B) is over the 400MB limit, not saving cache."
|
||||
expect(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);
|
||||
});
|
||||
|
||||
test("save with reserve cache failure outputs warning", async () => {
|
||||
const infoMock = jest.spyOn(core, "info");
|
||||
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
|
||||
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
||||
const cacheEntry: ArtifactCacheEntry = {
|
||||
cacheKey: "Linux-node-",
|
||||
scope: "refs/heads/master",
|
||||
creationTime: "2019-11-13T19:18:02+00:00",
|
||||
archiveLocation: "www.actionscache.test/download"
|
||||
};
|
||||
|
||||
jest.spyOn(core, "getState")
|
||||
// Cache Entry State
|
||||
.mockImplementationOnce(() => {
|
||||
return JSON.stringify(cacheEntry);
|
||||
})
|
||||
// Cache Key State
|
||||
.mockImplementationOnce(() => {
|
||||
return primaryKey;
|
||||
});
|
||||
|
||||
const inputPath = "node_modules";
|
||||
testUtils.setInput(Inputs.Path, inputPath);
|
||||
|
||||
const reserveCacheMock = jest
|
||||
.spyOn(cacheHttpClient, "reserveCache")
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve(-1);
|
||||
});
|
||||
|
||||
const createTarMock = jest.spyOn(tar, "createTar");
|
||||
|
||||
const saveCacheMock = jest.spyOn(cacheHttpClient, "saveCache");
|
||||
|
||||
await run();
|
||||
|
||||
expect(reserveCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey);
|
||||
|
||||
expect(infoMock).toHaveBeenCalledWith(
|
||||
`Unable to reserve cache with key ${primaryKey}, another job may be creating this cache.`
|
||||
);
|
||||
|
||||
expect(createTarMock).toHaveBeenCalledTimes(0);
|
||||
expect(saveCacheMock).toHaveBeenCalledTimes(0);
|
||||
expect(logWarningMock).toHaveBeenCalledTimes(0);
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test("save with server error outputs warning", async () => {
|
||||
const warningMock = jest.spyOn(core, "warning");
|
||||
const logWarningMock = jest.spyOn(actionUtils, "logWarning");
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
|
||||
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
||||
@ -234,7 +291,14 @@ test("save with server error outputs warning", async () => {
|
||||
const cachePath = path.resolve(inputPath);
|
||||
testUtils.setInput(Inputs.Path, inputPath);
|
||||
|
||||
const execMock = jest.spyOn(exec, "exec");
|
||||
const 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")
|
||||
@ -244,35 +308,24 @@ test("save with server error outputs warning", async () => {
|
||||
|
||||
await run();
|
||||
|
||||
expect(reserveCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey);
|
||||
|
||||
const archivePath = path.join("/foo/bar", "cache.tgz");
|
||||
|
||||
const IS_WINDOWS = process.platform === "win32";
|
||||
const args = IS_WINDOWS
|
||||
? [
|
||||
"-cz",
|
||||
"--force-local",
|
||||
"-f",
|
||||
archivePath.replace(/\\/g, "/"),
|
||||
"-C",
|
||||
cachePath.replace(/\\/g, "/"),
|
||||
"."
|
||||
]
|
||||
: ["-cz", "-f", archivePath, "-C", cachePath, "."];
|
||||
|
||||
expect(execMock).toHaveBeenCalledTimes(1);
|
||||
expect(execMock).toHaveBeenCalledWith(`"tar"`, args);
|
||||
expect(createTarMock).toHaveBeenCalledTimes(1);
|
||||
expect(createTarMock).toHaveBeenCalledWith(archivePath, cachePath);
|
||||
|
||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(saveCacheMock).toHaveBeenCalledWith(primaryKey, archivePath);
|
||||
expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archivePath);
|
||||
|
||||
expect(warningMock).toHaveBeenCalledTimes(1);
|
||||
expect(warningMock).toHaveBeenCalledWith("HTTP Error Occurred");
|
||||
expect(logWarningMock).toHaveBeenCalledTimes(1);
|
||||
expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
|
||||
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
test("save with valid inputs uploads a cache", async () => {
|
||||
const warningMock = jest.spyOn(core, "warning");
|
||||
const failedMock = jest.spyOn(core, "setFailed");
|
||||
|
||||
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
|
||||
@ -297,33 +350,29 @@ test("save with valid inputs uploads a cache", async () => {
|
||||
const cachePath = path.resolve(inputPath);
|
||||
testUtils.setInput(Inputs.Path, inputPath);
|
||||
|
||||
const execMock = jest.spyOn(exec, "exec");
|
||||
const cacheId = 4;
|
||||
const reserveCacheMock = jest
|
||||
.spyOn(cacheHttpClient, "reserveCache")
|
||||
.mockImplementationOnce(() => {
|
||||
return Promise.resolve(cacheId);
|
||||
});
|
||||
|
||||
const createTarMock = jest.spyOn(tar, "createTar");
|
||||
|
||||
const saveCacheMock = jest.spyOn(cacheHttpClient, "saveCache");
|
||||
|
||||
await run();
|
||||
|
||||
expect(reserveCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey);
|
||||
|
||||
const archivePath = path.join("/foo/bar", "cache.tgz");
|
||||
|
||||
const IS_WINDOWS = process.platform === "win32";
|
||||
const args = IS_WINDOWS
|
||||
? [
|
||||
"-cz",
|
||||
"--force-local",
|
||||
"-f",
|
||||
archivePath.replace(/\\/g, "/"),
|
||||
"-C",
|
||||
cachePath.replace(/\\/g, "/"),
|
||||
"."
|
||||
]
|
||||
: ["-cz", "-f", archivePath, "-C", cachePath, "."];
|
||||
|
||||
expect(execMock).toHaveBeenCalledTimes(1);
|
||||
expect(execMock).toHaveBeenCalledWith(`"tar"`, args);
|
||||
expect(createTarMock).toHaveBeenCalledTimes(1);
|
||||
expect(createTarMock).toHaveBeenCalledWith(archivePath, cachePath);
|
||||
|
||||
expect(saveCacheMock).toHaveBeenCalledTimes(1);
|
||||
expect(saveCacheMock).toHaveBeenCalledWith(primaryKey, archivePath);
|
||||
expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archivePath);
|
||||
|
||||
expect(warningMock).toHaveBeenCalledTimes(0);
|
||||
expect(failedMock).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
86
__tests__/tar.test.ts
Normal file
86
__tests__/tar.test.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import * as exec from "@actions/exec";
|
||||
import * as io from "@actions/io";
|
||||
import * as tar from "../src/tar";
|
||||
|
||||
import fs = require("fs");
|
||||
|
||||
jest.mock("@actions/exec");
|
||||
jest.mock("@actions/io");
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(io, "which").mockImplementation(tool => {
|
||||
return Promise.resolve(tool);
|
||||
});
|
||||
});
|
||||
|
||||
test("extract BSD tar", async () => {
|
||||
const mkdirMock = jest.spyOn(io, "mkdirP");
|
||||
const execMock = jest.spyOn(exec, "exec");
|
||||
|
||||
const IS_WINDOWS = process.platform === "win32";
|
||||
const archivePath = IS_WINDOWS
|
||||
? `${process.env["windir"]}\\fakepath\\cache.tar`
|
||||
: "cache.tar";
|
||||
const targetDirectory = "~/.npm/cache";
|
||||
await tar.extractTar(archivePath, targetDirectory);
|
||||
|
||||
expect(mkdirMock).toHaveBeenCalledWith(targetDirectory);
|
||||
|
||||
const tarPath = IS_WINDOWS
|
||||
? `${process.env["windir"]}\\System32\\tar.exe`
|
||||
: "tar";
|
||||
expect(execMock).toHaveBeenCalledTimes(1);
|
||||
expect(execMock).toHaveBeenCalledWith(`"${tarPath}"`, [
|
||||
"-xz",
|
||||
"-f",
|
||||
IS_WINDOWS ? archivePath.replace(/\\/g, "/") : archivePath,
|
||||
"-C",
|
||||
IS_WINDOWS ? targetDirectory?.replace(/\\/g, "/") : targetDirectory
|
||||
]);
|
||||
});
|
||||
|
||||
test("extract GNU tar", async () => {
|
||||
const IS_WINDOWS = process.platform === "win32";
|
||||
if (IS_WINDOWS) {
|
||||
jest.spyOn(fs, "existsSync").mockReturnValueOnce(false);
|
||||
jest.spyOn(tar, "isGnuTar").mockReturnValue(Promise.resolve(true));
|
||||
|
||||
const execMock = jest.spyOn(exec, "exec");
|
||||
const archivePath = `${process.env["windir"]}\\fakepath\\cache.tar`;
|
||||
const targetDirectory = "~/.npm/cache";
|
||||
|
||||
await tar.extractTar(archivePath, targetDirectory);
|
||||
|
||||
expect(execMock).toHaveBeenCalledTimes(1);
|
||||
expect(execMock).toHaveBeenLastCalledWith(`"tar"`, [
|
||||
"-xz",
|
||||
"-f",
|
||||
archivePath.replace(/\\/g, "/"),
|
||||
"-C",
|
||||
targetDirectory?.replace(/\\/g, "/"),
|
||||
"--force-local"
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test("create BSD 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",
|
||||
IS_WINDOWS ? archivePath.replace(/\\/g, "/") : archivePath,
|
||||
"-C",
|
||||
IS_WINDOWS ? sourceDirectory?.replace(/\\/g, "/") : sourceDirectory,
|
||||
"."
|
||||
]);
|
||||
});
|
30
__tests__/verify-cache-files.sh
Executable file
30
__tests__/verify-cache-files.sh
Executable file
@ -0,0 +1,30 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Validate args
|
||||
prefix="$1"
|
||||
if [ -z "$prefix" ]; then
|
||||
echo "Must supply prefix 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="test-cache/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
|
@ -1,5 +1,5 @@
|
||||
name: 'Cache'
|
||||
description: 'Cache dependencies and build outputs to improve workflow execution time'
|
||||
description: 'Cache artifacts like dependencies and build outputs to improve workflow execution time'
|
||||
author: 'GitHub'
|
||||
inputs:
|
||||
path:
|
||||
@ -21,4 +21,4 @@ runs:
|
||||
post-if: 'success()'
|
||||
branding:
|
||||
icon: 'archive'
|
||||
color: 'gray-dark'
|
||||
color: 'gray-dark'
|
||||
|
6041
dist/restore/index.js
vendored
6041
dist/restore/index.js
vendored
File diff suppressed because it is too large
Load Diff
6054
dist/save/index.js
vendored
6054
dist/save/index.js
vendored
File diff suppressed because it is too large
Load Diff
165
examples.md
165
examples.md
@ -1,27 +1,24 @@
|
||||
# Examples
|
||||
|
||||
- [Examples](#examples)
|
||||
- [C# - Nuget](#c---nuget)
|
||||
- [Elixir - Mix](#elixir---mix)
|
||||
- [Go - Modules](#go---modules)
|
||||
- [Java - Gradle](#java---gradle)
|
||||
- [Java - Maven](#java---maven)
|
||||
- [Node - npm](#node---npm)
|
||||
- [macOS and Ubuntu](#macos-and-ubuntu)
|
||||
- [Windows](#windows)
|
||||
- [Using multiple systems and `npm config`](#using-multiple-systems-and-npm-config)
|
||||
- [Node - Yarn](#node---yarn)
|
||||
- [PHP - Composer](#php---composer)
|
||||
- [Python - pip](#python---pip)
|
||||
- [Simple example](#simple-example)
|
||||
- [Multiple OS's in a workflow](#multiple-oss-in-a-workflow)
|
||||
- [Using a script to get cache location](#using-a-script-to-get-cache-location)
|
||||
- [Ruby - Gem](#ruby---gem)
|
||||
- [Rust - Cargo](#rust---cargo)
|
||||
- [Swift, Objective-C - Carthage](#swift-objective-c---carthage)
|
||||
- [Swift, Objective-C - CocoaPods](#swift-objective-c---cocoapods)
|
||||
- [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)
|
||||
|
||||
## C# - Nuget
|
||||
## C# - NuGet
|
||||
Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/package-references-in-project-files#locking-dependencies):
|
||||
|
||||
```yaml
|
||||
@ -33,6 +30,21 @@ Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/packa
|
||||
${{ runner.os }}-nuget-
|
||||
```
|
||||
|
||||
Depending on the environment, huge packages might be pre-installed in the global cache folder.
|
||||
If you do not want to include them, consider to move the cache folder like below.
|
||||
>Note: This workflow does not work for projects that require files to be placed in user profile package folder
|
||||
```yaml
|
||||
env:
|
||||
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
|
||||
steps:
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ${{ github.workspace }}/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-nuget-
|
||||
```
|
||||
|
||||
## Elixir - Mix
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
@ -54,6 +66,28 @@ Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/packa
|
||||
${{ runner.os }}-go-
|
||||
```
|
||||
|
||||
## Haskell - Cabal
|
||||
|
||||
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
|
||||
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
|
||||
```
|
||||
|
||||
## Java - Gradle
|
||||
|
||||
```yaml
|
||||
@ -216,15 +250,72 @@ Replace `~/.cache/pip` with the correct `path` if not using Ubuntu.
|
||||
${{ runner.os }}-pip-
|
||||
```
|
||||
|
||||
## Ruby - Gem
|
||||
## R - renv
|
||||
|
||||
For renv, the cache directory will vary by OS. Look at https://rstudio.github.io/renv/articles/renv.html#cache
|
||||
|
||||
Locations:
|
||||
- Ubuntu: `~/.local/share/renv`
|
||||
- macOS: `~/Library/Application Support/renv`
|
||||
- Windows: `%LOCALAPPDATA%/renv`
|
||||
|
||||
### Simple example
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: ~/.local/share/renv
|
||||
key: ${{ runner.os }}-renv-${{ hashFiles('**/renv.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-renv-
|
||||
```
|
||||
|
||||
Replace `~/.local/share/renv` with the correct `path` if not using Ubuntu.
|
||||
|
||||
### Multiple OS's in a workflow
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
if: startsWith(runner.os, 'Linux')
|
||||
with:
|
||||
path: ~/.local/share/renv
|
||||
key: ${{ runner.os }}-renv-${{ hashFiles('**/renv.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-renv-
|
||||
|
||||
- uses: actions/cache@v1
|
||||
if: startsWith(runner.os, 'macOS')
|
||||
with:
|
||||
path: ~/Library/Application Support/renv
|
||||
key: ${{ runner.os }}-renv-${{ hashFiles('**/renv.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-renv-
|
||||
|
||||
- uses: actions/cache@v1
|
||||
if: startsWith(runner.os, 'Windows')
|
||||
with:
|
||||
path: ~\AppData\Local\renv
|
||||
key: ${{ runner.os }}-renv-${{ hashFiles('**/renv.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-renv-
|
||||
```
|
||||
|
||||
## Ruby - Bundler
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: vendor/bundle
|
||||
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
|
||||
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-gem-
|
||||
${{ runner.os }}-gems-
|
||||
```
|
||||
When dependencies are installed later in the workflow, we must specify the same path for the bundler.
|
||||
|
||||
```yaml
|
||||
- name: Bundle install
|
||||
run: |
|
||||
bundle config path vendor/bundle
|
||||
bundle install --jobs 4 --retry 3
|
||||
```
|
||||
|
||||
## Rust - Cargo
|
||||
@ -247,6 +338,21 @@ Replace `~/.cache/pip` with the correct `path` if not using Ubuntu.
|
||||
key: ${{ runner.os }}-cargo-build-target-${{ 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
|
||||
with:
|
||||
path: ~/.sbt
|
||||
key: ${{ runner.os }}-sbt-${{ hashFiles('**/build.sbt') }}
|
||||
```
|
||||
|
||||
## Swift, Objective-C - Carthage
|
||||
|
||||
```yaml
|
||||
@ -268,3 +374,14 @@ Replace `~/.cache/pip` with the correct `path` if not using Ubuntu.
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pods-
|
||||
```
|
||||
|
||||
## Swift - Swift Package Manager
|
||||
|
||||
```yaml
|
||||
- uses: actions/cache@v1
|
||||
with:
|
||||
path: .build
|
||||
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-spm-
|
||||
```
|
||||
|
79
package-lock.json
generated
79
package-lock.json
generated
@ -1,19 +1,46 @@
|
||||
{
|
||||
"name": "cache",
|
||||
"version": "1.0.2",
|
||||
"version": "1.2.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@actions/core": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.0.tgz",
|
||||
"integrity": "sha512-ZKdyhlSlyz38S6YFfPnyNgCDZuAF2T0Qv5eHflNWytPS8Qjvz39bZFMry9Bb/dpSnqWcNeav5yM2CTYpJeY+Dw=="
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
|
||||
"requires": {
|
||||
"@actions/http-client": "^2.0.1",
|
||||
"uuid": "^8.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@actions/http-client": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.1.0.tgz",
|
||||
"integrity": "sha512-BonhODnXr3amchh4qkmjPMUO8mFi/zLaaCeCAJZqch8iQqyDnVIkySjB38VHAC8IJ+bnlgfOqlhpyCUZHlQsqw==",
|
||||
"requires": {
|
||||
"tunnel": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@actions/exec": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.1.tgz",
|
||||
"integrity": "sha512-nvFkxwiicvpzNiCBF4wFBDfnBvi7xp/as7LE1hBxBxKG2L29+gkIPBiLKMVORL+Hg3JNf07AKRfl0V5djoypjQ=="
|
||||
},
|
||||
"@actions/http-client": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.6.tgz",
|
||||
"integrity": "sha512-LGmio4w98UyGX33b/W6V6Nx/sQHRXZ859YlMkn36wPsXPB82u8xTVlA/Dq2DXrm6lEq9RVmisRJa1c+HETAIJA==",
|
||||
"requires": {
|
||||
"tunnel": "0.0.6"
|
||||
}
|
||||
},
|
||||
"@actions/io": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.1.tgz",
|
||||
@ -2854,9 +2881,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"handlebars": {
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.1.tgz",
|
||||
"integrity": "sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA==",
|
||||
"version": "4.5.3",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz",
|
||||
"integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"neo-async": "^2.6.0",
|
||||
@ -4859,9 +4886,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"prettier": {
|
||||
"version": "1.18.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz",
|
||||
"integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==",
|
||||
"version": "1.19.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz",
|
||||
"integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==",
|
||||
"dev": true
|
||||
},
|
||||
"prettier-linter-helpers": {
|
||||
@ -5933,9 +5960,9 @@
|
||||
}
|
||||
},
|
||||
"tunnel": {
|
||||
"version": "0.0.4",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.4.tgz",
|
||||
"integrity": "sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM="
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
|
||||
"integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
|
||||
},
|
||||
"tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
@ -5973,25 +6000,16 @@
|
||||
"integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==",
|
||||
"dev": true
|
||||
},
|
||||
"typed-rest-client": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.5.0.tgz",
|
||||
"integrity": "sha512-DVZRlmsfnTjp6ZJaatcdyvvwYwbWvR4YDNFDqb+qdTxpvaVP99YCpBkA8rxsLtAPjBVoDe4fNsnMIdZTiPuKWg==",
|
||||
"requires": {
|
||||
"tunnel": "0.0.4",
|
||||
"underscore": "1.8.3"
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "3.6.4",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz",
|
||||
"integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==",
|
||||
"version": "3.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz",
|
||||
"integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw==",
|
||||
"dev": true
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.6.7",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.7.tgz",
|
||||
"integrity": "sha512-4sXQDzmdnoXiO+xvmTzQsfIiwrjUCSA95rSP4SEd8tDb51W2TiDOlL76Hl+Kw0Ie42PSItCW8/t6pBNCF2R48A==",
|
||||
"version": "3.7.3",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.7.3.tgz",
|
||||
"integrity": "sha512-7tINm46/3puUA4hCkKYo4Xdts+JDaVC9ZPRcG8Xw9R4nhO/gZgUM3TENq8IF4Vatk8qCig4MzP/c8G4u2BkVQg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
@ -5999,11 +6017,6 @@
|
||||
"source-map": "~0.6.1"
|
||||
}
|
||||
},
|
||||
"underscore": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz",
|
||||
"integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI="
|
||||
},
|
||||
"union-value": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
|
||||
|
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "cache",
|
||||
"version": "1.0.2",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"description": "Cache dependencies and build outputs",
|
||||
"main": "dist/restore/index.js",
|
||||
@ -24,10 +24,10 @@
|
||||
"author": "GitHub",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.2.0",
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/exec": "^1.0.1",
|
||||
"@actions/http-client": "^1.0.6",
|
||||
"@actions/io": "^1.0.1",
|
||||
"typed-rest-client": "^1.5.0",
|
||||
"uuid": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -46,8 +46,8 @@
|
||||
"jest": "^24.8.0",
|
||||
"jest-circus": "^24.7.1",
|
||||
"nock": "^11.7.0",
|
||||
"prettier": "1.18.2",
|
||||
"prettier": "^1.19.1",
|
||||
"ts-jest": "^24.0.2",
|
||||
"typescript": "^3.6.4"
|
||||
"typescript": "^3.7.3"
|
||||
}
|
||||
}
|
||||
|
@ -1,26 +1,66 @@
|
||||
import * as core from "@actions/core";
|
||||
import { HttpClient, HttpCodes } from "@actions/http-client";
|
||||
import { BearerCredentialHandler } from "@actions/http-client/auth";
|
||||
import {
|
||||
IHttpClientResponse,
|
||||
IRequestOptions,
|
||||
ITypedResponse
|
||||
} from "@actions/http-client/interfaces";
|
||||
import * as fs from "fs";
|
||||
import { BearerCredentialHandler } from "typed-rest-client/Handlers";
|
||||
import { HttpClient } from "typed-rest-client/HttpClient";
|
||||
import { IHttpClientResponse } from "typed-rest-client/Interfaces";
|
||||
import { IRequestOptions, RestClient } from "typed-rest-client/RestClient";
|
||||
import { ArtifactCacheEntry } from "./contracts";
|
||||
import * as stream from "stream";
|
||||
import * as util from "util";
|
||||
|
||||
function getCacheUrl(): string {
|
||||
import { SocketTimeout } from "./constants";
|
||||
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 isServerErrorStatusCode(statusCode?: number): boolean {
|
||||
if (!statusCode) {
|
||||
return true;
|
||||
}
|
||||
return statusCode >= 500;
|
||||
}
|
||||
|
||||
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 cacheUrl: string = (
|
||||
const baseUrl: string = (
|
||||
process.env["ACTIONS_CACHE_URL"] ||
|
||||
process.env["ACTIONS_RUNTIME_URL"] ||
|
||||
""
|
||||
).replace("pipelines", "artifactcache");
|
||||
if (!cacheUrl) {
|
||||
if (!baseUrl) {
|
||||
throw new Error(
|
||||
"Cache Service Url not found, unable to restore cache."
|
||||
);
|
||||
}
|
||||
|
||||
core.debug(`Cache Url: ${cacheUrl}`);
|
||||
return cacheUrl;
|
||||
const url = `${baseUrl}_apis/artifactcache/${resource}`;
|
||||
core.debug(`Resource Url: ${url}`);
|
||||
return url;
|
||||
}
|
||||
|
||||
function createAcceptHeader(type: string, apiVersion: string): string {
|
||||
@ -29,99 +69,328 @@ function createAcceptHeader(type: string, apiVersion: string): string {
|
||||
|
||||
function getRequestOptions(): IRequestOptions {
|
||||
const requestOptions: IRequestOptions = {
|
||||
acceptHeader: createAcceptHeader("application/json", "5.2-preview.1")
|
||||
headers: {
|
||||
Accept: createAcceptHeader("application/json", "6.0-preview.1")
|
||||
}
|
||||
};
|
||||
|
||||
return requestOptions;
|
||||
}
|
||||
|
||||
export async function getCacheEntry(
|
||||
keys: string[]
|
||||
): Promise<ArtifactCacheEntry | null> {
|
||||
const cacheUrl = getCacheUrl();
|
||||
function createHttpClient(): HttpClient {
|
||||
const token = process.env["ACTIONS_RUNTIME_TOKEN"] || "";
|
||||
const bearerCredentialHandler = new BearerCredentialHandler(token);
|
||||
|
||||
const resource = `_apis/artifactcache/cache?keys=${encodeURIComponent(
|
||||
keys.join(",")
|
||||
)}`;
|
||||
|
||||
const restClient = new RestClient("actions/cache", cacheUrl, [
|
||||
bearerCredentialHandler
|
||||
]);
|
||||
|
||||
const response = await restClient.get<ArtifactCacheEntry>(
|
||||
resource,
|
||||
return new HttpClient(
|
||||
"actions/cache",
|
||||
[bearerCredentialHandler],
|
||||
getRequestOptions()
|
||||
);
|
||||
}
|
||||
|
||||
export async function retry<T>(
|
||||
name: string,
|
||||
method: () => Promise<T>,
|
||||
getStatusCode: (T) => number | undefined,
|
||||
maxAttempts = 2
|
||||
): Promise<T> {
|
||||
let response: T | undefined = undefined;
|
||||
let statusCode: number | undefined = undefined;
|
||||
let isRetryable = false;
|
||||
let errorMessage = "";
|
||||
let attempt = 1;
|
||||
|
||||
while (attempt <= maxAttempts) {
|
||||
try {
|
||||
response = await method();
|
||||
statusCode = getStatusCode(response);
|
||||
|
||||
if (!isServerErrorStatusCode(statusCode)) {
|
||||
return response;
|
||||
}
|
||||
|
||||
isRetryable = isRetryableStatusCode(statusCode);
|
||||
errorMessage = `Cache service responded with ${statusCode}`;
|
||||
} catch (error) {
|
||||
isRetryable = true;
|
||||
errorMessage = error.message;
|
||||
}
|
||||
|
||||
core.debug(
|
||||
`${name} - Attempt ${attempt} of ${maxAttempts} failed with error: ${errorMessage}`
|
||||
);
|
||||
|
||||
if (!isRetryable) {
|
||||
core.debug(`${name} - Error is not retryable`);
|
||||
break;
|
||||
}
|
||||
|
||||
attempt++;
|
||||
}
|
||||
|
||||
throw Error(`${name} failed: ${errorMessage}`);
|
||||
}
|
||||
|
||||
export async function retryTypedResponse<T>(
|
||||
name: string,
|
||||
method: () => Promise<ITypedResponse<T>>,
|
||||
maxAttempts = 2
|
||||
): Promise<ITypedResponse<T>> {
|
||||
return await retry(
|
||||
name,
|
||||
method,
|
||||
(response: ITypedResponse<T>) => response.statusCode,
|
||||
maxAttempts
|
||||
);
|
||||
}
|
||||
|
||||
export async function retryHttpClientResponse<T>(
|
||||
name: string,
|
||||
method: () => Promise<IHttpClientResponse>,
|
||||
maxAttempts = 2
|
||||
): Promise<IHttpClientResponse> {
|
||||
return await retry(
|
||||
name,
|
||||
method,
|
||||
(response: IHttpClientResponse) => response.message.statusCode,
|
||||
maxAttempts
|
||||
);
|
||||
}
|
||||
|
||||
export async function getCacheEntry(
|
||||
keys: string[]
|
||||
): Promise<ArtifactCacheEntry | null> {
|
||||
const httpClient = createHttpClient();
|
||||
const resource = `cache?keys=${encodeURIComponent(keys.join(","))}`;
|
||||
|
||||
const response = await retryTypedResponse("getCacheEntry", () =>
|
||||
httpClient.getJson<ArtifactCacheEntry>(getCacheApiUrl(resource))
|
||||
);
|
||||
if (response.statusCode === 204) {
|
||||
return null;
|
||||
}
|
||||
if (response.statusCode !== 200) {
|
||||
if (!isSuccessStatusCode(response.statusCode)) {
|
||||
throw new Error(`Cache service responded with ${response.statusCode}`);
|
||||
}
|
||||
|
||||
const cacheResult = response.result;
|
||||
core.debug(`Cache Result:`);
|
||||
core.debug(JSON.stringify(cacheResult));
|
||||
if (!cacheResult || !cacheResult.archiveLocation) {
|
||||
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
|
||||
output: NodeJS.WritableStream
|
||||
): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
response.message.pipe(stream).on("close", () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
const pipeline = util.promisify(stream.pipeline);
|
||||
await pipeline(response.message, output);
|
||||
}
|
||||
|
||||
export async function downloadCache(
|
||||
cacheEntry: ArtifactCacheEntry,
|
||||
archiveLocation: string,
|
||||
archivePath: string
|
||||
): Promise<void> {
|
||||
const stream = fs.createWriteStream(archivePath);
|
||||
const httpClient = new HttpClient("actions/cache");
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const downloadResponse = await httpClient.get(cacheEntry.archiveLocation!);
|
||||
const downloadResponse = await retryHttpClientResponse(
|
||||
"downloadCache",
|
||||
() => httpClient.get(archiveLocation)
|
||||
);
|
||||
|
||||
// Abort download if no traffic received over the socket.
|
||||
downloadResponse.message.socket.setTimeout(SocketTimeout, () => {
|
||||
downloadResponse.message.destroy();
|
||||
core.debug(
|
||||
`Aborting download, socket timed out after ${SocketTimeout} ms`
|
||||
);
|
||||
});
|
||||
|
||||
await pipeResponseToStream(downloadResponse, stream);
|
||||
|
||||
// Validate download size.
|
||||
const contentLengthHeader =
|
||||
downloadResponse.message.headers["content-length"];
|
||||
|
||||
if (contentLengthHeader) {
|
||||
const expectedLength = parseInt(contentLengthHeader);
|
||||
const actualLength = utils.getArchiveFileSize(archivePath);
|
||||
|
||||
if (actualLength != expectedLength) {
|
||||
throw new Error(
|
||||
`Incomplete download. Expected file size: ${expectedLength}, actual file size: ${actualLength}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
core.debug("Unable to validate download, no Content-Length header");
|
||||
}
|
||||
}
|
||||
|
||||
// Reserve Cache
|
||||
export async function reserveCache(key: string): Promise<number> {
|
||||
const httpClient = createHttpClient();
|
||||
|
||||
const reserveCacheRequest: ReserveCacheRequest = {
|
||||
key
|
||||
};
|
||||
const response = await retryTypedResponse("reserveCache", () =>
|
||||
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,
|
||||
openStream: () => 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)
|
||||
};
|
||||
|
||||
await retryHttpClientResponse(
|
||||
`uploadChunk (start: ${start}, end: ${end})`,
|
||||
() =>
|
||||
httpClient.sendStream(
|
||||
"PATCH",
|
||||
resourceUrl,
|
||||
openStream(),
|
||||
additionalHeaders
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
await uploadChunk(
|
||||
httpClient,
|
||||
resourceUrl,
|
||||
() =>
|
||||
fs
|
||||
.createReadStream(archivePath, {
|
||||
fd,
|
||||
start,
|
||||
end,
|
||||
autoClose: false
|
||||
})
|
||||
.on("error", error => {
|
||||
throw new Error(
|
||||
`Cache upload failed because file read failed with ${error.Message}`
|
||||
);
|
||||
}),
|
||||
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 retryTypedResponse("commitCache", () =>
|
||||
httpClient.postJson<null>(
|
||||
getCacheApiUrl(`caches/${cacheId.toString()}`),
|
||||
commitCacheRequest
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export async function saveCache(
|
||||
key: string,
|
||||
cacheId: number,
|
||||
archivePath: string
|
||||
): Promise<void> {
|
||||
const stream = fs.createReadStream(archivePath);
|
||||
const httpClient = createHttpClient();
|
||||
|
||||
const cacheUrl = getCacheUrl();
|
||||
const token = process.env["ACTIONS_RUNTIME_TOKEN"] || "";
|
||||
const bearerCredentialHandler = new BearerCredentialHandler(token);
|
||||
core.debug("Upload cache");
|
||||
await uploadFile(httpClient, cacheId, archivePath);
|
||||
|
||||
const resource = `_apis/artifactcache/cache/${encodeURIComponent(key)}`;
|
||||
const postUrl = cacheUrl + resource;
|
||||
|
||||
const restClient = new RestClient("actions/cache", undefined, [
|
||||
bearerCredentialHandler
|
||||
]);
|
||||
|
||||
const requestOptions = getRequestOptions();
|
||||
requestOptions.additionalHeaders = {
|
||||
"Content-Type": "application/octet-stream"
|
||||
};
|
||||
|
||||
const response = await restClient.uploadStream<void>(
|
||||
"POST",
|
||||
postUrl,
|
||||
stream,
|
||||
requestOptions
|
||||
// Commit Cache
|
||||
core.debug("Commiting cache");
|
||||
const cacheSize = utils.getArchiveFileSize(archivePath);
|
||||
const commitCacheResponse = await commitCache(
|
||||
httpClient,
|
||||
cacheId,
|
||||
cacheSize
|
||||
);
|
||||
if (response.statusCode !== 200) {
|
||||
throw new Error(`Cache service responded with ${response.statusCode}`);
|
||||
if (!isSuccessStatusCode(commitCacheResponse.statusCode)) {
|
||||
throw new Error(
|
||||
`Cache service responded with ${commitCacheResponse.statusCode} during commit cache.`
|
||||
);
|
||||
}
|
||||
|
||||
core.info("Cache saved successfully");
|
||||
|
@ -18,3 +18,8 @@ export enum Events {
|
||||
Push = "push",
|
||||
PullRequest = "pull_request"
|
||||
}
|
||||
|
||||
// Socket timeout in milliseconds during download. If no traffic is received
|
||||
// over the socket during this period, the socket is destroyed and the download
|
||||
// is aborted.
|
||||
export const SocketTimeout = 5000;
|
||||
|
13
src/contracts.d.ts
vendored
13
src/contracts.d.ts
vendored
@ -4,3 +4,16 @@ export interface ArtifactCacheEntry {
|
||||
creationTime?: string;
|
||||
archiveLocation?: string;
|
||||
}
|
||||
|
||||
export interface CommitCacheRequest {
|
||||
size: number;
|
||||
}
|
||||
|
||||
export interface ReserveCacheRequest {
|
||||
key: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export interface ReserveCacheResponse {
|
||||
cacheId: number;
|
||||
}
|
||||
|
@ -1,22 +1,22 @@
|
||||
import * as core from "@actions/core";
|
||||
import { exec } from "@actions/exec";
|
||||
import * as io from "@actions/io";
|
||||
import * as path from "path";
|
||||
import * as cacheHttpClient from "./cacheHttpClient";
|
||||
import { Events, Inputs, State } from "./constants";
|
||||
import { extractTar } from "./tar";
|
||||
import * as utils from "./utils/actionUtils";
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
// Validate inputs, this can cause task failure
|
||||
if (!utils.isValidEvent()) {
|
||||
core.setFailed(
|
||||
utils.logWarning(
|
||||
`Event Validation Error: The event type ${
|
||||
process.env[Events.Key]
|
||||
} is not supported. Only ${utils
|
||||
.getSupportedEvents()
|
||||
.join(", ")} events are supported at this time.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const cachePath = utils.resolvePath(
|
||||
@ -60,7 +60,7 @@ async function run(): Promise<void> {
|
||||
|
||||
try {
|
||||
const cacheEntry = await cacheHttpClient.getCacheEntry(keys);
|
||||
if (!cacheEntry) {
|
||||
if (!cacheEntry?.archiveLocation) {
|
||||
core.info(
|
||||
`Cache not found for input keys: ${keys.join(", ")}.`
|
||||
);
|
||||
@ -77,7 +77,10 @@ async function run(): Promise<void> {
|
||||
utils.setCacheState(cacheEntry);
|
||||
|
||||
// Download the cache from the cache entry
|
||||
await cacheHttpClient.downloadCache(cacheEntry, archivePath);
|
||||
await cacheHttpClient.downloadCache(
|
||||
cacheEntry.archiveLocation,
|
||||
archivePath
|
||||
);
|
||||
|
||||
const archiveFileSize = utils.getArchiveFileSize(archivePath);
|
||||
core.info(
|
||||
@ -86,27 +89,7 @@ async function run(): Promise<void> {
|
||||
)} MB (${archiveFileSize} B)`
|
||||
);
|
||||
|
||||
// Create directory to extract tar into
|
||||
await io.mkdirP(cachePath);
|
||||
|
||||
// http://man7.org/linux/man-pages/man1/tar.1.html
|
||||
// tar [-options] <name of the tar archive> [files or directories which to add into archive]
|
||||
const IS_WINDOWS = process.platform === "win32";
|
||||
const args = IS_WINDOWS
|
||||
? [
|
||||
"-xz",
|
||||
"--force-local",
|
||||
"-f",
|
||||
archivePath.replace(/\\/g, "/"),
|
||||
"-C",
|
||||
cachePath.replace(/\\/g, "/")
|
||||
]
|
||||
: ["-xz", "-f", archivePath, "-C", cachePath];
|
||||
|
||||
const tarPath = await io.which("tar", true);
|
||||
core.debug(`Tar Path: ${tarPath}`);
|
||||
|
||||
await exec(`"${tarPath}"`, args);
|
||||
await extractTar(archivePath, cachePath);
|
||||
|
||||
const isExactKeyMatch = utils.isExactKeyMatch(
|
||||
primaryKey,
|
||||
@ -118,7 +101,7 @@ async function run(): Promise<void> {
|
||||
`Cache restored from key: ${cacheEntry && cacheEntry.cacheKey}`
|
||||
);
|
||||
} catch (error) {
|
||||
core.warning(error.message);
|
||||
utils.logWarning(error.message);
|
||||
utils.setCacheHitOutput(false);
|
||||
}
|
||||
} catch (error) {
|
||||
|
57
src/save.ts
57
src/save.ts
@ -1,19 +1,29 @@
|
||||
import * as core from "@actions/core";
|
||||
import { exec } from "@actions/exec";
|
||||
import * as io from "@actions/io";
|
||||
import * as path from "path";
|
||||
import * as cacheHttpClient from "./cacheHttpClient";
|
||||
import { Inputs, State } from "./constants";
|
||||
import { Events, Inputs, State } from "./constants";
|
||||
import { createTar } from "./tar";
|
||||
import * as utils from "./utils/actionUtils";
|
||||
|
||||
async function run(): Promise<void> {
|
||||
try {
|
||||
if (!utils.isValidEvent()) {
|
||||
utils.logWarning(
|
||||
`Event Validation Error: The event type ${
|
||||
process.env[Events.Key]
|
||||
} is not supported. Only ${utils
|
||||
.getSupportedEvents()
|
||||
.join(", ")} events are supported at this time.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const state = utils.getCacheState();
|
||||
|
||||
// Inputs are re-evaluted before the post action, so we want the original key used for restore
|
||||
const primaryKey = core.getState(State.CacheKey);
|
||||
if (!primaryKey) {
|
||||
core.warning(`Error retrieving key from state.`);
|
||||
utils.logWarning(`Error retrieving key from state.`);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -24,6 +34,15 @@ 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;
|
||||
}
|
||||
core.debug(`Cache ID: ${cacheId}`);
|
||||
const cachePath = utils.resolvePath(
|
||||
core.getInput(Inputs.Path, { required: true })
|
||||
);
|
||||
@ -35,40 +54,24 @@ async function run(): Promise<void> {
|
||||
);
|
||||
core.debug(`Archive Path: ${archivePath}`);
|
||||
|
||||
// http://man7.org/linux/man-pages/man1/tar.1.html
|
||||
// tar [-options] <name of the tar archive> [files or directories which to add into archive]
|
||||
const IS_WINDOWS = process.platform === "win32";
|
||||
const args = IS_WINDOWS
|
||||
? [
|
||||
"-cz",
|
||||
"--force-local",
|
||||
"-f",
|
||||
archivePath.replace(/\\/g, "/"),
|
||||
"-C",
|
||||
cachePath.replace(/\\/g, "/"),
|
||||
"."
|
||||
]
|
||||
: ["-cz", "-f", archivePath, "-C", cachePath, "."];
|
||||
await createTar(archivePath, cachePath);
|
||||
|
||||
const tarPath = await io.which("tar", true);
|
||||
core.debug(`Tar Path: ${tarPath}`);
|
||||
await exec(`"${tarPath}"`, args);
|
||||
|
||||
const fileSizeLimit = 400 * 1024 * 1024; // 400MB
|
||||
const fileSizeLimit = 5 * 1024 * 1024 * 1024; // 5GB per repo limit
|
||||
const archiveFileSize = utils.getArchiveFileSize(archivePath);
|
||||
core.debug(`File Size: ${archiveFileSize}`);
|
||||
if (archiveFileSize > fileSizeLimit) {
|
||||
core.warning(
|
||||
utils.logWarning(
|
||||
`Cache size of ~${Math.round(
|
||||
archiveFileSize / (1024 * 1024)
|
||||
)} MB (${archiveFileSize} B) is over the 400MB limit, not saving cache.`
|
||||
)} MB (${archiveFileSize} B) is over the 5GB limit, not saving cache.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
await cacheHttpClient.saveCache(primaryKey, archivePath);
|
||||
core.debug(`Saving Cache (ID: ${cacheId})`);
|
||||
await cacheHttpClient.saveCache(cacheId, archivePath);
|
||||
} catch (error) {
|
||||
core.warning(error.message);
|
||||
utils.logWarning(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
76
src/tar.ts
Normal file
76
src/tar.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import * as core from "@actions/core";
|
||||
import { exec } from "@actions/exec";
|
||||
import * as io from "@actions/io";
|
||||
import { existsSync } from "fs";
|
||||
import * as path from "path";
|
||||
import * as tar from "./tar";
|
||||
|
||||
export async function isGnuTar(): Promise<boolean> {
|
||||
core.debug("Checking tar --version");
|
||||
let versionOutput = "";
|
||||
await exec("tar --version", [], {
|
||||
ignoreReturnCode: true,
|
||||
silent: true,
|
||||
listeners: {
|
||||
stdout: (data: Buffer): string =>
|
||||
(versionOutput += data.toString()),
|
||||
stderr: (data: Buffer): string => (versionOutput += data.toString())
|
||||
}
|
||||
});
|
||||
|
||||
core.debug(versionOutput.trim());
|
||||
return versionOutput.toUpperCase().includes("GNU TAR");
|
||||
}
|
||||
|
||||
async function getTarPath(args: string[]): 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;
|
||||
} else if (await tar.isGnuTar()) {
|
||||
args.push("--force-local");
|
||||
}
|
||||
}
|
||||
return await io.which("tar", true);
|
||||
}
|
||||
|
||||
async function execTar(args: string[]): Promise<void> {
|
||||
try {
|
||||
await exec(`"${await getTarPath(args)}"`, args);
|
||||
} catch (error) {
|
||||
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.replace(new RegExp("\\" + path.sep, "g"), "/"),
|
||||
"-C",
|
||||
targetDirectory.replace(new RegExp("\\" + path.sep, "g"), "/")
|
||||
];
|
||||
await execTar(args);
|
||||
}
|
||||
|
||||
export async function createTar(
|
||||
archivePath: string,
|
||||
sourceDirectory: string
|
||||
): Promise<void> {
|
||||
const args = [
|
||||
"-cz",
|
||||
"-f",
|
||||
archivePath.replace(new RegExp("\\" + path.sep, "g"), "/"),
|
||||
"-C",
|
||||
sourceDirectory.replace(new RegExp("\\" + path.sep, "g"), "/"),
|
||||
"."
|
||||
];
|
||||
await execTar(args);
|
||||
}
|
@ -77,6 +77,11 @@ export function getCacheState(): ArtifactCacheEntry | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function logWarning(message: string): void {
|
||||
const warningPrefix = "[warning]";
|
||||
core.info(`${warningPrefix}${message}`);
|
||||
}
|
||||
|
||||
export function resolvePath(filePath: string): string {
|
||||
if (filePath[0] === "~") {
|
||||
const home = os.homedir();
|
||||
|
Reference in New Issue
Block a user