Compare commits

...

29 Commits

Author SHA1 Message Date
d9747005de Merge pull request #308 from actions/aiyan/v1-release
Cherry-pick commits for v1
2020-05-14 11:11:05 -04:00
3f662ca624 Add Eric's e2e test change to get more coverage 2020-05-12 17:14:33 -04:00
0232e3178d Add retries to all API calls 2020-05-12 16:16:48 -04:00
ee7a57c615 error handling for stream 2020-05-11 16:48:56 -04:00
da9f90cb83 Fix upload chunk retries 2020-05-11 14:46:06 -04:00
ec7f7ebd08 Use promisify of stream.pipeline for downloading 2020-05-11 14:41:48 -04:00
2a973a0f4e Add comment for SocketTimeout 2020-05-11 14:32:38 -04:00
cbbb8b4d4f Fix lint issue, build .js files 2020-05-11 14:31:13 -04:00
5a0add1806 Adds socket timeout and validate file size 2020-05-11 14:28:28 -04:00
9fe7ad8b07 Use path.sep in path replace 2020-05-11 14:20:33 -04:00
7c7d003bbb Rebase and rebuild 2020-05-11 14:08:26 -04:00
96e5a46c57 Fix test 2020-05-11 14:08:26 -04:00
84e606dfac Fallback to GNU tar if BSD tar is unavailable 2020-05-11 14:08:23 -04:00
70655ec832 Release v1.1.2 2020-02-05 10:41:57 -05:00
78a4b2143b Bump version to 1.1.2 2020-02-05 10:40:53 -05:00
4dc4b4e758 Change name back to Cache 2020-02-05 10:39:52 -05:00
85aee6a487 Update docs with 5GB limit 2020-02-05 10:33:21 -05:00
fe1055e9d1 Release v1.1.1 2020-02-05 10:01:01 -05:00
fab26f3f4f Bump version to 1.1.1 2020-02-05 09:55:35 -05:00
4887979af8 proxy support (#166)
* Replace typed rest client with new http-client

* Send Content-Type: application/json and fix up some types

* Lint

* Consume @actions/http-client:1.0.5

* Consume @actions/http-client:1.0.6

* Dont send headers manually, http-client automatically will
2020-02-05 09:24:37 -05:00
f9c9166ecb Increase cache limit to 5 GBs (#168)
* Increase cache limit to 5 GBs

* Fix test to use new size limit

* Update src/save.ts

Co-Authored-By: Josh Gross <joshmgross@github.com>

Co-authored-by: Josh Gross <joshmgross@github.com>
2020-02-01 16:11:02 -05:00
23e301d35c Disable fail-fast to get full coverage of failures 2020-01-29 20:34:56 -05:00
e43776276f Add Swift Package Manager (SPM) example (#159)
* Add Swift - SPM to examples

* Add link SPM example link to readme

* remove extra newline

* remove another extra newline
2020-01-29 11:13:59 -05:00
b6d538e2aa Add renv examples (#151)
* Add renv examples

* Add link in main readme.md
2020-01-21 19:22:40 -05:00
296374f6c9 Update action's description (#75)
* README: clarify case on the action

* Update description
2020-01-14 10:11:41 -05:00
6c11532937 Update Ruby docs. "Gem" -> "Bundler" (#150)
* Use "Bundler" which is the package manager

"Gem" isn't wrong, but not typically what a Ruby developer would think of.

* Update links

* Update links
2020-01-12 18:48:43 -05:00
c33bff8d72 Add Scala - SBT example (#134)
* Add Scala - SBT example

* Add Scala - SBT example to README
2020-01-10 17:09:06 -05:00
d1991bb4c5 Add Haskell - Cabal example (#148)
* Add Haskell - Cabal example

* Add link in main readme.md
2020-01-10 17:07:52 -05:00
60e292adf7 Update cache limits (#140) 2020-01-07 15:01:47 -05:00
18 changed files with 2258 additions and 6088 deletions

View File

@ -4,50 +4,130 @@ on:
pull_request: pull_request:
branches: branches:
- master - master
- releases/**
paths-ignore: paths-ignore:
- '**.md' - '**.md'
push: push:
branches: branches:
- master - master
- releases/**
paths-ignore: paths-ignore:
- '**.md' - '**.md'
jobs: jobs:
test: # Build and unit test
name: Test on ${{ matrix.os }} build:
strategy: strategy:
matrix: matrix:
os: [ubuntu-latest, windows-latest, macOS-latest] os: [ubuntu-latest, windows-latest, macOS-latest]
fail-fast: false
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v1 - name: Checkout
uses: actions/checkout@v2
- uses: actions/setup-node@v1 - name: Setup Node.js
uses: actions/setup-node@v1
with: with:
node-version: '12.x' node-version: '12.x'
- name: Determine npm cache directory
- name: Get npm cache directory
id: npm-cache id: npm-cache
run: | run: |
echo "::set-output name=dir::$(npm config get cache)" echo "::set-output name=dir::$(npm config get cache)"
- name: Restore npm cache
- uses: actions/cache@v1 uses: actions/cache@v1
with: with:
path: ${{ steps.npm-cache.outputs.dir }} path: ${{ steps.npm-cache.outputs.dir }}
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: | restore-keys: |
${{ runner.os }}-node- ${{ runner.os }}-node-
- run: npm ci - run: npm ci
- name: Prettier Format Check - name: Prettier Format Check
run: npm run format-check run: npm run format-check
- name: ESLint Check - name: ESLint Check
run: npm run lint run: npm run lint
- name: Build & Test - name: Build & Test
run: npm run 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: 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
- 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

3
.gitignore vendored
View File

@ -94,3 +94,6 @@ typings/
# DynamoDB Local files # DynamoDB Local files
.dynamodb/ .dynamodb/
# Text editor files
.vscode/

View File

@ -1,6 +1,6 @@
# cache # 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/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=master&event=push"></a>
@ -63,21 +63,24 @@ See [Examples](examples.md) for a list of `actions/cache` implementations for us
- [C# - Nuget](./examples.md#c---nuget) - [C# - Nuget](./examples.md#c---nuget)
- [Elixir - Mix](./examples.md#elixir---mix) - [Elixir - Mix](./examples.md#elixir---mix)
- [Go - Modules](./examples.md#go---modules) - [Go - Modules](./examples.md#go---modules)
- [Haskell - Cabal](./examples.md#haskell---cabal)
- [Java - Gradle](./examples.md#java---gradle) - [Java - Gradle](./examples.md#java---gradle)
- [Java - Maven](./examples.md#java---maven) - [Java - Maven](./examples.md#java---maven)
- [Node - npm](./examples.md#node---npm) - [Node - npm](./examples.md#node---npm)
- [Node - Yarn](./examples.md#node---yarn) - [Node - Yarn](./examples.md#node---yarn)
- [PHP - Composer](./examples.md#php---composer) - [PHP - Composer](./examples.md#php---composer)
- [Python - pip](./examples.md#python---pip) - [Python - pip](./examples.md#python---pip)
- [Ruby - Gem](./examples.md#ruby---gem) - [R - renv](./examples.md#r---renv)
- [Ruby - Bundler](./examples.md#ruby---bundler)
- [Rust - Cargo](./examples.md#rust---cargo) - [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 - Carthage](./examples.md#swift-objective-c---carthage)
- [Swift, Objective-C - CocoaPods](./examples.md#swift-objective-c---cocoapods) - [Swift, Objective-C - CocoaPods](./examples.md#swift-objective-c---cocoapods)
- [Swift - Swift Package Manager](./examples.md#swift---swift-package-manager)
## Cache Limits ## Cache Limits
Individual caches are limited to 400MB and a repository can have up to 2GB of caches. Once the 2GB limit is reached, older caches will be evicted based on when the cache was last accessed. Caches that are not accessed within the last week will also be evicted. 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 ## Skipping steps based on cache-hit

View 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
View 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

View File

@ -194,7 +194,7 @@ test("save with large cache outputs warning", async () => {
const createTarMock = jest.spyOn(tar, "createTar"); const createTarMock = jest.spyOn(tar, "createTar");
const cacheSize = 4 * 1024 * 1024 * 1024; //~4GB, over the 2GB limit const cacheSize = 6 * 1024 * 1024 * 1024; //~6GB, over the 5GB limit
jest.spyOn(actionUtils, "getArchiveFileSize").mockImplementationOnce(() => { jest.spyOn(actionUtils, "getArchiveFileSize").mockImplementationOnce(() => {
return cacheSize; return cacheSize;
}); });
@ -208,7 +208,7 @@ test("save with large cache outputs warning", async () => {
expect(logWarningMock).toHaveBeenCalledTimes(1); expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(logWarningMock).toHaveBeenCalledWith( expect(logWarningMock).toHaveBeenCalledWith(
"Cache size of ~4096 MB (4294967296 B) is over the 2GB limit, not saving cache." "Cache size of ~6144 MB (6442450944 B) is over the 5GB limit, not saving cache."
); );
expect(failedMock).toHaveBeenCalledTimes(0); expect(failedMock).toHaveBeenCalledTimes(0);

View File

@ -2,6 +2,8 @@ import * as exec from "@actions/exec";
import * as io from "@actions/io"; import * as io from "@actions/io";
import * as tar from "../src/tar"; import * as tar from "../src/tar";
import fs = require("fs");
jest.mock("@actions/exec"); jest.mock("@actions/exec");
jest.mock("@actions/io"); jest.mock("@actions/io");
@ -11,17 +13,19 @@ beforeAll(() => {
}); });
}); });
test("extract tar", async () => { test("extract BSD tar", async () => {
const mkdirMock = jest.spyOn(io, "mkdirP"); const mkdirMock = jest.spyOn(io, "mkdirP");
const execMock = jest.spyOn(exec, "exec"); const execMock = jest.spyOn(exec, "exec");
const archivePath = "cache.tar"; const IS_WINDOWS = process.platform === "win32";
const archivePath = IS_WINDOWS
? `${process.env["windir"]}\\fakepath\\cache.tar`
: "cache.tar";
const targetDirectory = "~/.npm/cache"; const targetDirectory = "~/.npm/cache";
await tar.extractTar(archivePath, targetDirectory); await tar.extractTar(archivePath, targetDirectory);
expect(mkdirMock).toHaveBeenCalledWith(targetDirectory); expect(mkdirMock).toHaveBeenCalledWith(targetDirectory);
const IS_WINDOWS = process.platform === "win32";
const tarPath = IS_WINDOWS const tarPath = IS_WINDOWS
? `${process.env["windir"]}\\System32\\tar.exe` ? `${process.env["windir"]}\\System32\\tar.exe`
: "tar"; : "tar";
@ -29,13 +33,37 @@ test("extract tar", async () => {
expect(execMock).toHaveBeenCalledWith(`"${tarPath}"`, [ expect(execMock).toHaveBeenCalledWith(`"${tarPath}"`, [
"-xz", "-xz",
"-f", "-f",
archivePath, IS_WINDOWS ? archivePath.replace(/\\/g, "/") : archivePath,
"-C", "-C",
targetDirectory IS_WINDOWS ? targetDirectory?.replace(/\\/g, "/") : targetDirectory
]); ]);
}); });
test("create tar", async () => { 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 execMock = jest.spyOn(exec, "exec");
const archivePath = "cache.tar"; const archivePath = "cache.tar";
@ -50,9 +78,9 @@ test("create tar", async () => {
expect(execMock).toHaveBeenCalledWith(`"${tarPath}"`, [ expect(execMock).toHaveBeenCalledWith(`"${tarPath}"`, [
"-cz", "-cz",
"-f", "-f",
archivePath, IS_WINDOWS ? archivePath.replace(/\\/g, "/") : archivePath,
"-C", "-C",
sourceDirectory, IS_WINDOWS ? sourceDirectory?.replace(/\\/g, "/") : sourceDirectory,
"." "."
]); ]);
}); });

30
__tests__/verify-cache-files.sh Executable file
View 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

View File

@ -1,5 +1,5 @@
name: 'Cache' 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' author: 'GitHub'
inputs: inputs:
path: path:

3703
dist/restore/index.js vendored

File diff suppressed because it is too large Load Diff

3707
dist/save/index.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -3,16 +3,20 @@
- [C# - NuGet](#c---nuget) - [C# - NuGet](#c---nuget)
- [Elixir - Mix](#elixir---mix) - [Elixir - Mix](#elixir---mix)
- [Go - Modules](#go---modules) - [Go - Modules](#go---modules)
- [Haskell - Cabal](#haskell---cabal)
- [Java - Gradle](#java---gradle) - [Java - Gradle](#java---gradle)
- [Java - Maven](#java---maven) - [Java - Maven](#java---maven)
- [Node - npm](#node---npm) - [Node - npm](#node---npm)
- [Node - Yarn](#node---yarn) - [Node - Yarn](#node---yarn)
- [PHP - Composer](#php---composer) - [PHP - Composer](#php---composer)
- [Python - pip](#python---pip) - [Python - pip](#python---pip)
- [Ruby - Gem](#ruby---gem) - [R - renv](#r---renv)
- [Ruby - Bundler](#ruby---bundler)
- [Rust - Cargo](#rust---cargo) - [Rust - Cargo](#rust---cargo)
- [Scala - SBT](#scala---sbt)
- [Swift, Objective-C - Carthage](#swift-objective-c---carthage) - [Swift, Objective-C - Carthage](#swift-objective-c---carthage)
- [Swift, Objective-C - CocoaPods](#swift-objective-c---cocoapods) - [Swift, Objective-C - CocoaPods](#swift-objective-c---cocoapods)
- [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): Using [NuGet lock files](https://docs.microsoft.com/nuget/consume-packages/package-references-in-project-files#locking-dependencies):
@ -62,6 +66,28 @@ steps:
${{ runner.os }}-go- ${{ 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 ## Java - Gradle
```yaml ```yaml
@ -224,15 +250,64 @@ Replace `~/.cache/pip` with the correct `path` if not using Ubuntu.
${{ runner.os }}-pip- ${{ 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 ```yaml
- uses: actions/cache@v1 - uses: actions/cache@v1
with: with:
path: vendor/bundle path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }} key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: | 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. When dependencies are installed later in the workflow, we must specify the same path for the bundler.
@ -263,6 +338,21 @@ When dependencies are installed later in the workflow, we must specify the same
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} 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 ## Swift, Objective-C - Carthage
```yaml ```yaml
@ -284,3 +374,14 @@ When dependencies are installed later in the workflow, we must specify the same
restore-keys: | restore-keys: |
${{ runner.os }}-pods- ${{ 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-
```

30
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "cache", "name": "cache",
"version": "1.1.0", "version": "1.1.2",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -14,6 +14,14 @@
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.1.tgz",
"integrity": "sha512-nvFkxwiicvpzNiCBF4wFBDfnBvi7xp/as7LE1hBxBxKG2L29+gkIPBiLKMVORL+Hg3JNf07AKRfl0V5djoypjQ==" "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": { "@actions/io": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@actions/io/-/io-1.0.1.tgz",
@ -5933,9 +5941,9 @@
} }
}, },
"tunnel": { "tunnel": {
"version": "0.0.4", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.4.tgz", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz",
"integrity": "sha1-LTeFoVjBdMmhbcLARuxfxfF0IhM=" "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="
}, },
"tunnel-agent": { "tunnel-agent": {
"version": "0.6.0", "version": "0.6.0",
@ -5973,15 +5981,6 @@
"integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==", "integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw==",
"dev": true "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": { "typescript": {
"version": "3.7.3", "version": "3.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz",
@ -5999,11 +5998,6 @@
"source-map": "~0.6.1" "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": { "union-value": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "cache", "name": "cache",
"version": "1.1.0", "version": "1.1.2",
"private": true, "private": true,
"description": "Cache dependencies and build outputs", "description": "Cache dependencies and build outputs",
"main": "dist/restore/index.js", "main": "dist/restore/index.js",
@ -26,8 +26,8 @@
"dependencies": { "dependencies": {
"@actions/core": "^1.2.0", "@actions/core": "^1.2.0",
"@actions/exec": "^1.0.1", "@actions/exec": "^1.0.1",
"@actions/http-client": "^1.0.6",
"@actions/io": "^1.0.1", "@actions/io": "^1.0.1",
"typed-rest-client": "^1.5.0",
"uuid": "^3.3.3" "uuid": "^3.3.3"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,13 +1,16 @@
import * as core from "@actions/core"; import * as core from "@actions/core";
import * as fs from "fs"; import { HttpClient, HttpCodes } from "@actions/http-client";
import { BearerCredentialHandler } from "typed-rest-client/Handlers"; import { BearerCredentialHandler } from "@actions/http-client/auth";
import { HttpClient, HttpCodes } from "typed-rest-client/HttpClient";
import { IHttpClientResponse } from "typed-rest-client/Interfaces";
import { import {
IHttpClientResponse,
IRequestOptions, IRequestOptions,
RestClient, ITypedResponse
IRestResponse } from "@actions/http-client/interfaces";
} from "typed-rest-client/RestClient"; import * as fs from "fs";
import * as stream from "stream";
import * as util from "util";
import { SocketTimeout } from "./constants";
import { import {
ArtifactCacheEntry, ArtifactCacheEntry,
CommitCacheRequest, CommitCacheRequest,
@ -16,11 +19,24 @@ import {
} from "./contracts"; } from "./contracts";
import * as utils from "./utils/actionUtils"; import * as utils from "./utils/actionUtils";
function isSuccessStatusCode(statusCode: number): boolean { function isSuccessStatusCode(statusCode?: number): boolean {
if (!statusCode) {
return false;
}
return statusCode >= 200 && statusCode < 300; return statusCode >= 200 && statusCode < 300;
} }
function isRetryableStatusCode(statusCode: number): boolean { function isServerErrorStatusCode(statusCode?: number): boolean {
if (!statusCode) {
return true;
}
return statusCode >= 500;
}
function isRetryableStatusCode(statusCode?: number): boolean {
if (!statusCode) {
return false;
}
const retryableStatusCodes = [ const retryableStatusCodes = [
HttpCodes.BadGateway, HttpCodes.BadGateway,
HttpCodes.ServiceUnavailable, HttpCodes.ServiceUnavailable,
@ -29,7 +45,7 @@ function isRetryableStatusCode(statusCode: number): boolean {
return retryableStatusCodes.includes(statusCode); return retryableStatusCodes.includes(statusCode);
} }
function getCacheApiUrl(): string { function getCacheApiUrl(resource: string): string {
// Ideally we just use ACTIONS_CACHE_URL // Ideally we just use ACTIONS_CACHE_URL
const baseUrl: string = ( const baseUrl: string = (
process.env["ACTIONS_CACHE_URL"] || process.env["ACTIONS_CACHE_URL"] ||
@ -42,8 +58,9 @@ function getCacheApiUrl(): string {
); );
} }
core.debug(`Cache Url: ${baseUrl}`); const url = `${baseUrl}_apis/artifactcache/${resource}`;
return `${baseUrl}_apis/artifactcache/`; core.debug(`Resource Url: ${url}`);
return url;
} }
function createAcceptHeader(type: string, apiVersion: string): string { function createAcceptHeader(type: string, apiVersion: string): string {
@ -52,30 +69,102 @@ function createAcceptHeader(type: string, apiVersion: string): string {
function getRequestOptions(): IRequestOptions { function getRequestOptions(): IRequestOptions {
const requestOptions: IRequestOptions = { const requestOptions: IRequestOptions = {
acceptHeader: createAcceptHeader("application/json", "6.0-preview.1") headers: {
Accept: createAcceptHeader("application/json", "6.0-preview.1")
}
}; };
return requestOptions; return requestOptions;
} }
function createRestClient(): RestClient { function createHttpClient(): HttpClient {
const token = process.env["ACTIONS_RUNTIME_TOKEN"] || ""; const token = process.env["ACTIONS_RUNTIME_TOKEN"] || "";
const bearerCredentialHandler = new BearerCredentialHandler(token); const bearerCredentialHandler = new BearerCredentialHandler(token);
return new RestClient("actions/cache", getCacheApiUrl(), [ return new HttpClient(
bearerCredentialHandler "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( export async function getCacheEntry(
keys: string[] keys: string[]
): Promise<ArtifactCacheEntry | null> { ): Promise<ArtifactCacheEntry | null> {
const restClient = createRestClient(); const httpClient = createHttpClient();
const resource = `cache?keys=${encodeURIComponent(keys.join(","))}`; const resource = `cache?keys=${encodeURIComponent(keys.join(","))}`;
const response = await restClient.get<ArtifactCacheEntry>( const response = await retryTypedResponse("getCacheEntry", () =>
resource, httpClient.getJson<ArtifactCacheEntry>(getCacheApiUrl(resource))
getRequestOptions()
); );
if (response.statusCode === 204) { if (response.statusCode === 204) {
return null; return null;
@ -83,6 +172,7 @@ export async function getCacheEntry(
if (!isSuccessStatusCode(response.statusCode)) { if (!isSuccessStatusCode(response.statusCode)) {
throw new Error(`Cache service responded with ${response.statusCode}`); throw new Error(`Cache service responded with ${response.statusCode}`);
} }
const cacheResult = response.result; const cacheResult = response.result;
const cacheDownloadUrl = cacheResult?.archiveLocation; const cacheDownloadUrl = cacheResult?.archiveLocation;
if (!cacheDownloadUrl) { if (!cacheDownloadUrl) {
@ -97,13 +187,10 @@ export async function getCacheEntry(
async function pipeResponseToStream( async function pipeResponseToStream(
response: IHttpClientResponse, response: IHttpClientResponse,
stream: NodeJS.WritableStream output: NodeJS.WritableStream
): Promise<void> { ): Promise<void> {
return new Promise(resolve => { const pipeline = util.promisify(stream.pipeline);
response.message.pipe(stream).on("close", () => { await pipeline(response.message, output);
resolve();
});
});
} }
export async function downloadCache( export async function downloadCache(
@ -112,23 +199,52 @@ export async function downloadCache(
): Promise<void> { ): Promise<void> {
const stream = fs.createWriteStream(archivePath); const stream = fs.createWriteStream(archivePath);
const httpClient = new HttpClient("actions/cache"); const httpClient = new HttpClient("actions/cache");
const downloadResponse = await httpClient.get(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); 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 // Reserve Cache
export async function reserveCache(key: string): Promise<number> { export async function reserveCache(key: string): Promise<number> {
const restClient = createRestClient(); const httpClient = createHttpClient();
const reserveCacheRequest: ReserveCacheRequest = { const reserveCacheRequest: ReserveCacheRequest = {
key key
}; };
const response = await restClient.create<ReserveCacheResponse>( const response = await retryTypedResponse("reserveCache", () =>
"caches", httpClient.postJson<ReserveCacheResponse>(
reserveCacheRequest, getCacheApiUrl("caches"),
getRequestOptions() reserveCacheRequest
)
); );
return response?.result?.cacheId ?? -1; return response?.result?.cacheId ?? -1;
} }
@ -142,9 +258,9 @@ function getContentRange(start: number, end: number): string {
} }
async function uploadChunk( async function uploadChunk(
restClient: RestClient, httpClient: HttpClient,
resourceUrl: string, resourceUrl: string,
data: NodeJS.ReadableStream, openStream: () => NodeJS.ReadableStream,
start: number, start: number,
end: number end: number
): Promise<void> { ): Promise<void> {
@ -156,38 +272,20 @@ async function uploadChunk(
end end
)}` )}`
); );
const requestOptions = getRequestOptions(); const additionalHeaders = {
requestOptions.additionalHeaders = {
"Content-Type": "application/octet-stream", "Content-Type": "application/octet-stream",
"Content-Range": getContentRange(start, end) "Content-Range": getContentRange(start, end)
}; };
const uploadChunkRequest = async (): Promise<IRestResponse<void>> => { await retryHttpClientResponse(
return await restClient.uploadStream<void>( `uploadChunk (start: ${start}, end: ${end})`,
() =>
httpClient.sendStream(
"PATCH", "PATCH",
resourceUrl, resourceUrl,
data, openStream(),
requestOptions additionalHeaders
); )
};
const response = await uploadChunkRequest();
if (isSuccessStatusCode(response.statusCode)) {
return;
}
if (isRetryableStatusCode(response.statusCode)) {
core.debug(
`Received ${response.statusCode}, retrying chunk at offset ${start}.`
);
const retryResponse = await uploadChunkRequest();
if (isSuccessStatusCode(retryResponse.statusCode)) {
return;
}
}
throw new Error(
`Cache service responded with ${response.statusCode} during chunk upload.`
); );
} }
@ -200,13 +298,13 @@ function parseEnvNumber(key: string): number | undefined {
} }
async function uploadFile( async function uploadFile(
restClient: RestClient, httpClient: HttpClient,
cacheId: number, cacheId: number,
archivePath: string archivePath: string
): Promise<void> { ): Promise<void> {
// Upload Chunks // Upload Chunks
const fileSize = fs.statSync(archivePath).size; const fileSize = fs.statSync(archivePath).size;
const resourceUrl = getCacheApiUrl() + "caches/" + cacheId.toString(); const resourceUrl = getCacheApiUrl(`caches/${cacheId.toString()}`);
const fd = fs.openSync(archivePath, "r"); const fd = fs.openSync(archivePath, "r");
const concurrency = parseEnvNumber("CACHE_UPLOAD_CONCURRENCY") ?? 4; // # of HTTP requests in parallel const concurrency = parseEnvNumber("CACHE_UPLOAD_CONCURRENCY") ?? 4; // # of HTTP requests in parallel
@ -229,17 +327,23 @@ async function uploadFile(
const start = offset; const start = offset;
const end = offset + chunkSize - 1; const end = offset + chunkSize - 1;
offset += MAX_CHUNK_SIZE; offset += MAX_CHUNK_SIZE;
const chunk = fs.createReadStream(archivePath, {
await uploadChunk(
httpClient,
resourceUrl,
() =>
fs
.createReadStream(archivePath, {
fd, fd,
start, start,
end, end,
autoClose: false autoClose: false
}); })
.on("error", error => {
await uploadChunk( throw new Error(
restClient, `Cache upload failed because file read failed with ${error.Message}`
resourceUrl, );
chunk, }),
start, start,
end end
); );
@ -253,16 +357,16 @@ async function uploadFile(
} }
async function commitCache( async function commitCache(
restClient: RestClient, httpClient: HttpClient,
cacheId: number, cacheId: number,
filesize: number filesize: number
): Promise<IRestResponse<void>> { ): Promise<ITypedResponse<null>> {
const requestOptions = getRequestOptions();
const commitCacheRequest: CommitCacheRequest = { size: filesize }; const commitCacheRequest: CommitCacheRequest = { size: filesize };
return await restClient.create( return await retryTypedResponse("commitCache", () =>
`caches/${cacheId.toString()}`, httpClient.postJson<null>(
commitCacheRequest, getCacheApiUrl(`caches/${cacheId.toString()}`),
requestOptions commitCacheRequest
)
); );
} }
@ -270,16 +374,16 @@ export async function saveCache(
cacheId: number, cacheId: number,
archivePath: string archivePath: string
): Promise<void> { ): Promise<void> {
const restClient = createRestClient(); const httpClient = createHttpClient();
core.debug("Upload cache"); core.debug("Upload cache");
await uploadFile(restClient, cacheId, archivePath); await uploadFile(httpClient, cacheId, archivePath);
// Commit Cache // Commit Cache
core.debug("Commiting cache"); core.debug("Commiting cache");
const cacheSize = utils.getArchiveFileSize(archivePath); const cacheSize = utils.getArchiveFileSize(archivePath);
const commitCacheResponse = await commitCache( const commitCacheResponse = await commitCache(
restClient, httpClient,
cacheId, cacheId,
cacheSize cacheSize
); );

View File

@ -18,3 +18,8 @@ export enum Events {
Push = "push", Push = "push",
PullRequest = "pull_request" 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;

View File

@ -56,14 +56,14 @@ async function run(): Promise<void> {
await createTar(archivePath, cachePath); await createTar(archivePath, cachePath);
const fileSizeLimit = 2 * 1024 * 1024 * 1024; // 2GB per repo limit const fileSizeLimit = 5 * 1024 * 1024 * 1024; // 5GB per repo limit
const archiveFileSize = utils.getArchiveFileSize(archivePath); const archiveFileSize = utils.getArchiveFileSize(archivePath);
core.debug(`File Size: ${archiveFileSize}`); core.debug(`File Size: ${archiveFileSize}`);
if (archiveFileSize > fileSizeLimit) { if (archiveFileSize > fileSizeLimit) {
utils.logWarning( utils.logWarning(
`Cache size of ~${Math.round( `Cache size of ~${Math.round(
archiveFileSize / (1024 * 1024) archiveFileSize / (1024 * 1024)
)} MB (${archiveFileSize} B) is over the 2GB limit, not saving cache.` )} MB (${archiveFileSize} B) is over the 5GB limit, not saving cache.`
); );
return; return;
} }

View File

@ -1,14 +1,36 @@
import * as core from "@actions/core";
import { exec } from "@actions/exec"; import { exec } from "@actions/exec";
import * as io from "@actions/io"; import * as io from "@actions/io";
import { existsSync } from "fs"; import { existsSync } from "fs";
import * as path from "path";
import * as tar from "./tar";
async function getTarPath(): Promise<string> { 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 // Explicitly use BSD Tar on Windows
const IS_WINDOWS = process.platform === "win32"; const IS_WINDOWS = process.platform === "win32";
if (IS_WINDOWS) { if (IS_WINDOWS) {
const systemTar = `${process.env["windir"]}\\System32\\tar.exe`; const systemTar = `${process.env["windir"]}\\System32\\tar.exe`;
if (existsSync(systemTar)) { if (existsSync(systemTar)) {
return systemTar; return systemTar;
} else if (await tar.isGnuTar()) {
args.push("--force-local");
} }
} }
return await io.which("tar", true); return await io.which("tar", true);
@ -16,14 +38,8 @@ async function getTarPath(): Promise<string> {
async function execTar(args: string[]): Promise<void> { async function execTar(args: string[]): Promise<void> {
try { try {
await exec(`"${await getTarPath()}"`, args); await exec(`"${await getTarPath(args)}"`, args);
} catch (error) { } 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}`); throw new Error(`Tar failed with error: ${error?.message}`);
} }
} }
@ -34,7 +50,13 @@ export async function extractTar(
): Promise<void> { ): Promise<void> {
// Create directory to extract tar into // Create directory to extract tar into
await io.mkdirP(targetDirectory); await io.mkdirP(targetDirectory);
const args = ["-xz", "-f", archivePath, "-C", targetDirectory]; const args = [
"-xz",
"-f",
archivePath.replace(new RegExp("\\" + path.sep, "g"), "/"),
"-C",
targetDirectory.replace(new RegExp("\\" + path.sep, "g"), "/")
];
await execTar(args); await execTar(args);
} }
@ -42,6 +64,13 @@ export async function createTar(
archivePath: string, archivePath: string,
sourceDirectory: string sourceDirectory: string
): Promise<void> { ): Promise<void> {
const args = ["-cz", "-f", archivePath, "-C", sourceDirectory, "."]; const args = [
"-cz",
"-f",
archivePath.replace(new RegExp("\\" + path.sep, "g"), "/"),
"-C",
sourceDirectory.replace(new RegExp("\\" + path.sep, "g"), "/"),
"."
];
await execTar(args); await execTar(args);
} }