Cache multiple paths and add glob pattern support (#212)

* Allow for multiple line-delimited paths to cache

* Add initial minimatch support

* Use @actions/glob for pattern matching

* Cache multiple entries using --files-from tar input

remove known failing test

Quote tar paths

Add salt to test cache

Try reading input files from manifest

bump salt

Run test on macos

more testing

Run caching tests on 3 platforms

Run tests on self-hosted

Apparently cant reference hosted runners by name

Bump salt

wait for some time after save

more timing out

smarter waiting

Cache in tmp dir that won't be deleted

Use child_process instead of actions/exec

Revert tempDir hack

bump salt

more logging

More console logging

Use filepath to with cacheHttpClient

Test cache restoration

Revert temp dir hack

debug logging

clean up cache.yml testing

Bump salt

change debug output

build actions

* unit test coverage for caching multiple dirs

* Ensure there's a locateable test folder at homedir

* Clean up code

* Version cache with all inputs

* Unit test getCacheVersion

* Include keys in getCacheEntry request

* Clean import orders

* Use fs promises in actionUtils tests

* Update import order for to fix linter errors

* Fix remaining linter error

* Remove platform-specific test code

* Add lerna example for caching multiple dirs

* Lerna example updated to v2

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

Co-authored-by: Josh Gross <joshmgross@github.com>
pull/233/head
Ethan Dennis 2020-03-20 13:02:11 -07:00 committed by GitHub
parent 22d71e33ad
commit eb78578266
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 4820 additions and 160 deletions

View File

@ -1,5 +1,6 @@
import * as core from "@actions/core";
import * as fs from "fs";
import * as io from "@actions/io";
import { promises as fs } from "fs";
import * as os from "os";
import * as path from "path";
@ -7,13 +8,24 @@ import { Events, Outputs, State } from "../src/constants";
import { ArtifactCacheEntry } from "../src/contracts";
import * as actionUtils from "../src/utils/actionUtils";
import uuid = require("uuid");
jest.mock("@actions/core");
jest.mock("os");
function getTempDir(): string {
return path.join(__dirname, "_temp", "actionUtils");
}
afterEach(() => {
delete process.env[Events.Key];
});
afterAll(async () => {
delete process.env["GITHUB_WORKSPACE"];
await io.rmRF(getTempDir());
});
test("getArchiveFileSize returns file size", () => {
const filePath = path.join(__dirname, "__fixtures__", "helloWorld.txt");
@ -182,17 +194,43 @@ test("isValidEvent returns false for unknown event", () => {
expect(isValidEvent).toBe(false);
});
test("resolvePath with no ~ in path", () => {
const filePath = ".cache/yarn";
test("resolvePaths with no ~ in path", async () => {
const filePath = ".cache";
const resolvedPath = actionUtils.resolvePath(filePath);
// Create the following layout:
// cwd
// cwd/.cache
// cwd/.cache/file.txt
const expectedPath = path.resolve(filePath);
expect(resolvedPath).toBe(expectedPath);
const root = path.join(getTempDir(), "no-tilde");
// tarball entries will be relative to workspace
process.env["GITHUB_WORKSPACE"] = root;
await fs.mkdir(root, { recursive: true });
const cache = path.join(root, ".cache");
await fs.mkdir(cache, { recursive: true });
await fs.writeFile(path.join(cache, "file.txt"), "cached");
const originalCwd = process.cwd();
try {
process.chdir(root);
const resolvedPath = await actionUtils.resolvePaths([filePath]);
const expectedPath = [filePath];
expect(resolvedPath).toStrictEqual(expectedPath);
} finally {
process.chdir(originalCwd);
}
});
test("resolvePath with ~ in path", () => {
const filePath = "~/.cache/yarn";
test("resolvePaths with ~ in path", async () => {
const cacheDir = uuid();
const filePath = `~/${cacheDir}`;
// Create the following layout:
// ~/uuid
// ~/uuid/file.txt
const homedir = jest.requireActual("os").homedir();
const homedirMock = jest.spyOn(os, "homedir");
@ -200,24 +238,93 @@ test("resolvePath with ~ in path", () => {
return homedir;
});
const resolvedPath = actionUtils.resolvePath(filePath);
const target = path.join(homedir, cacheDir);
await fs.mkdir(target, { recursive: true });
await fs.writeFile(path.join(target, "file.txt"), "cached");
const expectedPath = path.join(homedir, ".cache/yarn");
expect(resolvedPath).toBe(expectedPath);
const root = getTempDir();
process.env["GITHUB_WORKSPACE"] = root;
try {
const resolvedPath = await actionUtils.resolvePaths([filePath]);
const expectedPath = [path.relative(root, target)];
expect(resolvedPath).toStrictEqual(expectedPath);
} finally {
await io.rmRF(target);
}
});
test("resolvePath with home not found", () => {
test("resolvePaths with home not found", async () => {
const filePath = "~/.cache/yarn";
const homedirMock = jest.spyOn(os, "homedir");
homedirMock.mockImplementation(() => {
return "";
});
expect(() => actionUtils.resolvePath(filePath)).toThrow(
"Unable to resolve `~` to HOME"
await expect(actionUtils.resolvePaths([filePath])).rejects.toThrow(
"Unable to determine HOME directory"
);
});
test("resolvePaths inclusion pattern returns found", async () => {
const pattern = "*.ts";
// Create the following layout:
// inclusion-patterns
// inclusion-patterns/miss.txt
// inclusion-patterns/test.ts
const root = path.join(getTempDir(), "inclusion-patterns");
// tarball entries will be relative to workspace
process.env["GITHUB_WORKSPACE"] = root;
await fs.mkdir(root, { recursive: true });
await fs.writeFile(path.join(root, "miss.txt"), "no match");
await fs.writeFile(path.join(root, "test.ts"), "match");
const originalCwd = process.cwd();
try {
process.chdir(root);
const resolvedPath = await actionUtils.resolvePaths([pattern]);
const expectedPath = ["test.ts"];
expect(resolvedPath).toStrictEqual(expectedPath);
} finally {
process.chdir(originalCwd);
}
});
test("resolvePaths exclusion pattern returns not found", async () => {
const patterns = ["*.ts", "!test.ts"];
// Create the following layout:
// exclusion-patterns
// exclusion-patterns/miss.txt
// exclusion-patterns/test.ts
const root = path.join(getTempDir(), "exclusion-patterns");
// tarball entries will be relative to workspace
process.env["GITHUB_WORKSPACE"] = root;
await fs.mkdir(root, { recursive: true });
await fs.writeFile(path.join(root, "miss.txt"), "no match");
await fs.writeFile(path.join(root, "test.ts"), "no match");
const originalCwd = process.cwd();
try {
process.chdir(root);
const resolvedPath = await actionUtils.resolvePaths(patterns);
const expectedPath = [];
expect(resolvedPath).toStrictEqual(expectedPath);
} finally {
process.chdir(originalCwd);
}
});
test("isValidEvent returns true for push event", () => {
const event = Events.Push;
process.env[Events.Key] = event;
@ -237,13 +344,14 @@ test("isValidEvent returns true for pull request event", () => {
});
test("unlinkFile unlinks file", async () => {
const testDirectory = fs.mkdtempSync("unlinkFileTest");
const testDirectory = await fs.mkdtemp("unlinkFileTest");
const testFile = path.join(testDirectory, "test.txt");
fs.writeFileSync(testFile, "hello world");
await fs.writeFile(testFile, "hello world");
await actionUtils.unlinkFile(testFile);
expect(fs.existsSync(testFile)).toBe(false);
// This should throw as testFile should not exist
await expect(fs.stat(testFile)).rejects.toThrow();
fs.rmdirSync(testDirectory);
await fs.rmdir(testDirectory);
});

View File

@ -0,0 +1,21 @@
import { getCacheVersion } from "../src/cacheHttpClient";
import { Inputs } from "../src/constants";
import * as testUtils from "../src/utils/testUtils";
afterEach(() => {
testUtils.clearInputs();
});
test("getCacheVersion with path input returns version", async () => {
testUtils.setInput(Inputs.Path, "node_modules");
const result = getCacheVersion();
expect(result).toEqual(
"b3e0c6cb5ecf32614eeb2997d905b9c297046d7cbf69062698f25b14b4cb0985"
);
});
test("getCacheVersion with no input throws", async () => {
expect(() => getCacheVersion()).toThrow();
});

View File

@ -14,10 +14,6 @@ jest.mock("../src/tar");
jest.mock("../src/utils/actionUtils");
beforeAll(() => {
jest.spyOn(actionUtils, "resolvePath").mockImplementation(filePath => {
return path.resolve(filePath);
});
jest.spyOn(actionUtils, "isExactKeyMatch").mockImplementation(
(key, cacheResult) => {
const actualUtils = jest.requireActual("../src/utils/actionUtils");
@ -60,7 +56,8 @@ test("restore with invalid event outputs warning", async () => {
test("restore with no path should fail", async () => {
const failedMock = jest.spyOn(core, "setFailed");
await run();
expect(failedMock).toHaveBeenCalledWith(
// this input isn't necessary for restore b/c tarball contains entries relative to workspace
expect(failedMock).not.toHaveBeenCalledWith(
"Input required and not supplied: path"
);
});
@ -202,7 +199,6 @@ test("restore with restore keys and no cache found", async () => {
test("restore with cache found", async () => {
const key = "node-test";
const cachePath = path.resolve("node_modules");
testUtils.setInputs({
path: "node_modules",
key
@ -257,7 +253,7 @@ test("restore with cache found", async () => {
expect(getArchiveFileSizeMock).toHaveBeenCalledWith(archivePath);
expect(extractTarMock).toHaveBeenCalledTimes(1);
expect(extractTarMock).toHaveBeenCalledWith(archivePath, cachePath);
expect(extractTarMock).toHaveBeenCalledWith(archivePath);
expect(unlinkFileMock).toHaveBeenCalledTimes(1);
expect(unlinkFileMock).toHaveBeenCalledWith(archivePath);
@ -271,7 +267,6 @@ test("restore with cache found", async () => {
test("restore with a pull request event and cache found", async () => {
const key = "node-test";
const cachePath = path.resolve("node_modules");
testUtils.setInputs({
path: "node_modules",
key
@ -328,7 +323,7 @@ test("restore with a pull request event and cache found", async () => {
expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~60 MB (62915000 B)`);
expect(extractTarMock).toHaveBeenCalledTimes(1);
expect(extractTarMock).toHaveBeenCalledWith(archivePath, cachePath);
expect(extractTarMock).toHaveBeenCalledWith(archivePath);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith(true);
@ -340,7 +335,6 @@ test("restore with a pull request event and cache found", async () => {
test("restore with cache found for restore key", async () => {
const key = "node-test";
const restoreKey = "node-";
const cachePath = path.resolve("node_modules");
testUtils.setInputs({
path: "node_modules",
key,
@ -396,7 +390,7 @@ test("restore with cache found for restore key", async () => {
expect(infoMock).toHaveBeenCalledWith(`Cache Size: ~0 MB (142 B)`);
expect(extractTarMock).toHaveBeenCalledTimes(1);
expect(extractTarMock).toHaveBeenCalledWith(archivePath, cachePath);
expect(extractTarMock).toHaveBeenCalledWith(archivePath);
expect(setCacheHitOutputMock).toHaveBeenCalledTimes(1);
expect(setCacheHitOutputMock).toHaveBeenCalledWith(false);

View File

@ -2,7 +2,7 @@ import * as core from "@actions/core";
import * as path from "path";
import * as cacheHttpClient from "../src/cacheHttpClient";
import { Events, Inputs } from "../src/constants";
import { CacheFilename, Events, Inputs } from "../src/constants";
import { ArtifactCacheEntry } from "../src/contracts";
import run from "../src/save";
import * as tar from "../src/tar";
@ -41,9 +41,11 @@ beforeAll(() => {
return actualUtils.getSupportedEvents();
});
jest.spyOn(actionUtils, "resolvePath").mockImplementation(filePath => {
return path.resolve(filePath);
});
jest.spyOn(actionUtils, "resolvePaths").mockImplementation(
async filePaths => {
return filePaths.map(x => path.resolve(x));
}
);
jest.spyOn(actionUtils, "createTempDirectory").mockImplementation(() => {
return Promise.resolve("/foo/bar");
@ -190,7 +192,7 @@ test("save with large cache outputs warning", async () => {
});
const inputPath = "node_modules";
const cachePath = path.resolve(inputPath);
const cachePaths = [path.resolve(inputPath)];
testUtils.setInput(Inputs.Path, inputPath);
const createTarMock = jest.spyOn(tar, "createTar");
@ -202,10 +204,10 @@ test("save with large cache outputs warning", async () => {
await run();
const archivePath = path.join("/foo/bar", "cache.tgz");
const archiveFolder = "/foo/bar";
expect(createTarMock).toHaveBeenCalledTimes(1);
expect(createTarMock).toHaveBeenCalledWith(archivePath, cachePath);
expect(createTarMock).toHaveBeenCalledWith(archiveFolder, cachePaths);
expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(logWarningMock).toHaveBeenCalledWith(
@ -289,7 +291,7 @@ test("save with server error outputs warning", async () => {
});
const inputPath = "node_modules";
const cachePath = path.resolve(inputPath);
const cachePaths = [path.resolve(inputPath)];
testUtils.setInput(Inputs.Path, inputPath);
const cacheId = 4;
@ -312,13 +314,14 @@ test("save with server error outputs warning", async () => {
expect(reserveCacheMock).toHaveBeenCalledTimes(1);
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey);
const archivePath = path.join("/foo/bar", "cache.tgz");
const archiveFolder = "/foo/bar";
const archiveFile = path.join(archiveFolder, CacheFilename);
expect(createTarMock).toHaveBeenCalledTimes(1);
expect(createTarMock).toHaveBeenCalledWith(archivePath, cachePath);
expect(createTarMock).toHaveBeenCalledWith(archiveFolder, cachePaths);
expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archivePath);
expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archiveFile);
expect(logWarningMock).toHaveBeenCalledTimes(1);
expect(logWarningMock).toHaveBeenCalledWith("HTTP Error Occurred");
@ -348,7 +351,7 @@ test("save with valid inputs uploads a cache", async () => {
});
const inputPath = "node_modules";
const cachePath = path.resolve(inputPath);
const cachePaths = [path.resolve(inputPath)];
testUtils.setInput(Inputs.Path, inputPath);
const cacheId = 4;
@ -367,13 +370,14 @@ test("save with valid inputs uploads a cache", async () => {
expect(reserveCacheMock).toHaveBeenCalledTimes(1);
expect(reserveCacheMock).toHaveBeenCalledWith(primaryKey);
const archivePath = path.join("/foo/bar", "cache.tgz");
const archiveFolder = "/foo/bar";
const archiveFile = path.join(archiveFolder, CacheFilename);
expect(createTarMock).toHaveBeenCalledTimes(1);
expect(createTarMock).toHaveBeenCalledWith(archivePath, cachePath);
expect(createTarMock).toHaveBeenCalledWith(archiveFolder, cachePaths);
expect(saveCacheMock).toHaveBeenCalledTimes(1);
expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archivePath);
expect(saveCacheMock).toHaveBeenCalledWith(cacheId, archiveFile);
expect(failedMock).toHaveBeenCalledTimes(0);
});

View File

@ -1,15 +1,30 @@
import * as exec from "@actions/exec";
import * as io from "@actions/io";
import { promises as fs } from "fs";
import * as path from "path";
import { CacheFilename } from "../src/constants";
import * as tar from "../src/tar";
jest.mock("@actions/exec");
jest.mock("@actions/io");
beforeAll(() => {
function getTempDir(): string {
return path.join(__dirname, "_temp", "tar");
}
beforeAll(async () => {
jest.spyOn(io, "which").mockImplementation(tool => {
return Promise.resolve(tool);
});
process.env["GITHUB_WORKSPACE"] = process.cwd();
await jest.requireActual("@actions/io").rmRF(getTempDir());
});
afterAll(async () => {
delete process.env["GITHUB_WORKSPACE"];
await jest.requireActual("@actions/io").rmRF(getTempDir());
});
test("extract tar", async () => {
@ -17,43 +32,54 @@ test("extract tar", async () => {
const execMock = jest.spyOn(exec, "exec");
const archivePath = "cache.tar";
const targetDirectory = "~/.npm/cache";
await tar.extractTar(archivePath, targetDirectory);
const workspace = process.env["GITHUB_WORKSPACE"];
expect(mkdirMock).toHaveBeenCalledWith(targetDirectory);
await tar.extractTar(archivePath);
expect(mkdirMock).toHaveBeenCalledWith(workspace);
const IS_WINDOWS = process.platform === "win32";
const tarPath = IS_WINDOWS
? `${process.env["windir"]}\\System32\\tar.exe`
: "tar";
expect(execMock).toHaveBeenCalledTimes(1);
expect(execMock).toHaveBeenCalledWith(`"${tarPath}"`, [
"-xz",
"-f",
archivePath,
"-C",
targetDirectory
]);
expect(execMock).toHaveBeenCalledWith(
`"${tarPath}"`,
["-xz", "-f", archivePath, "-P", "-C", workspace],
{ cwd: undefined }
);
});
test("create tar", async () => {
const execMock = jest.spyOn(exec, "exec");
const archivePath = "cache.tar";
const sourceDirectory = "~/.npm/cache";
await tar.createTar(archivePath, sourceDirectory);
const archiveFolder = getTempDir();
const workspace = process.env["GITHUB_WORKSPACE"];
const sourceDirectories = ["~/.npm/cache", `${workspace}/dist`];
await fs.mkdir(archiveFolder, { recursive: true });
await tar.createTar(archiveFolder, sourceDirectories);
const IS_WINDOWS = process.platform === "win32";
const tarPath = IS_WINDOWS
? `${process.env["windir"]}\\System32\\tar.exe`
: "tar";
expect(execMock).toHaveBeenCalledTimes(1);
expect(execMock).toHaveBeenCalledWith(`"${tarPath}"`, [
"-cz",
"-f",
archivePath,
"-C",
sourceDirectory,
"."
]);
expect(execMock).toHaveBeenCalledWith(
`"${tarPath}"`,
[
"-cz",
"-f",
CacheFilename,
"-C",
workspace,
"--files-from",
"manifest.txt"
],
{
cwd: archiveFolder
}
);
});

2264
dist/restore/index.js vendored

File diff suppressed because it is too large Load Diff

2273
dist/save/index.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,7 @@
- [Windows](#windows)
- [Using multiple systems and `npm config`](#using-multiple-systems-and-npm-config)
- [Node - Yarn](#node---yarn)
- [Node - Lerna](#node---lerna)
- [OCaml/Reason - esy](#ocamlreason---esy)
- [PHP - Composer](#php---composer)
- [Python - pip](#python---pip)
@ -184,6 +185,18 @@ The yarn cache directory will depend on your operating system and version of `ya
${{ runner.os }}-yarn-
```
## Node - Lerna
```yaml
- name: restore lerna
uses: actions/cache@v2
with:
path: |
node_modules
*/*/node_modules
key: ${{ runner.os }}-${{ hashFiles('yarn.lock') }}
```
## OCaml/Reason - esy
Esy allows you to export built dependencies and import pre-built dependencies.
```yaml

17
package-lock.json generated
View File

@ -14,6 +14,15 @@
"resolved": "https://registry.npmjs.org/@actions/exec/-/exec-1.0.1.tgz",
"integrity": "sha512-nvFkxwiicvpzNiCBF4wFBDfnBvi7xp/as7LE1hBxBxKG2L29+gkIPBiLKMVORL+Hg3JNf07AKRfl0V5djoypjQ=="
},
"@actions/glob": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.1.0.tgz",
"integrity": "sha512-lx8SzyQ2FE9+UUvjqY1f28QbTJv+w8qP7kHHbfQRhphrlcx0Mdmm1tZdGJzfxv1jxREa/sLW4Oy8CbGQKCJySA==",
"requires": {
"@actions/core": "^1.2.0",
"minimatch": "^3.0.4"
}
},
"@actions/http-client": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-1.0.6.tgz",
@ -1436,8 +1445,7 @@
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"dev": true
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"base": {
"version": "0.11.2",
@ -1513,7 +1521,6 @@
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -1800,8 +1807,7 @@
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"dev": true
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"contains-path": {
"version": "0.1.0",
@ -7156,7 +7162,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"dev": true,
"requires": {
"brace-expansion": "^1.1.7"
}

View File

@ -25,6 +25,7 @@
"dependencies": {
"@actions/core": "^1.2.0",
"@actions/exec": "^1.0.1",
"@actions/glob": "^0.1.0",
"@actions/http-client": "^1.0.6",
"@actions/io": "^1.0.1",
"uuid": "^3.3.3"

View File

@ -6,8 +6,10 @@ import {
IRequestOptions,
ITypedResponse
} from "@actions/http-client/interfaces";
import * as crypto from "crypto";
import * as fs from "fs";
import { Inputs } from "./constants";
import {
ArtifactCacheEntry,
CommitCacheRequest,
@ -16,6 +18,8 @@ import {
} from "./contracts";
import * as utils from "./utils/actionUtils";
const versionSalt = "1.0";
function isSuccessStatusCode(statusCode?: number): boolean {
if (!statusCode) {
return false;
@ -78,11 +82,27 @@ function createHttpClient(): HttpClient {
);
}
export function getCacheVersion(): string {
// Add salt to cache version to support breaking changes in cache entry
const components = [
core.getInput(Inputs.Path, { required: true }),
versionSalt
];
return crypto
.createHash("sha256")
.update(components.join("|"))
.digest("hex");
}
export async function getCacheEntry(
keys: string[]
): Promise<ArtifactCacheEntry | null> {
const httpClient = createHttpClient();
const resource = `cache?keys=${encodeURIComponent(keys.join(","))}`;
const version = getCacheVersion();
const resource = `cache?keys=${encodeURIComponent(
keys.join(",")
)}&version=${version}`;
const response = await httpClient.getJson<ArtifactCacheEntry>(
getCacheApiUrl(resource)
@ -130,9 +150,11 @@ export async function downloadCache(
// Reserve Cache
export async function reserveCache(key: string): Promise<number> {
const httpClient = createHttpClient();
const version = getCacheVersion();
const reserveCacheRequest: ReserveCacheRequest = {
key
key,
version
};
const response = await httpClient.postJson<ReserveCacheResponse>(
getCacheApiUrl("caches"),

View File

@ -18,3 +18,5 @@ export enum Events {
Push = "push",
PullRequest = "pull_request"
}
export const CacheFilename = "cache.tgz";

View File

@ -20,11 +20,6 @@ async function run(): Promise<void> {
return;
}
const cachePath = utils.resolvePath(
core.getInput(Inputs.Path, { required: true })
);
core.debug(`Cache Path: ${cachePath}`);
const primaryKey = core.getInput(Inputs.Key, { required: true });
core.saveState(State.CacheKey, primaryKey);
@ -89,7 +84,7 @@ async function run(): Promise<void> {
)} MB (${archiveFileSize} B)`
);
await extractTar(archivePath, cachePath);
await extractTar(archivePath);
} finally {
// Try to delete the archive to save space
try {

View File

@ -2,7 +2,7 @@ import * as core from "@actions/core";
import * as path from "path";
import * as cacheHttpClient from "./cacheHttpClient";
import { Events, Inputs, State } from "./constants";
import { CacheFilename, Events, Inputs, State } from "./constants";
import { createTar } from "./tar";
import * as utils from "./utils/actionUtils";
@ -44,18 +44,21 @@ async function run(): Promise<void> {
return;
}
core.debug(`Cache ID: ${cacheId}`);
const cachePath = utils.resolvePath(
core.getInput(Inputs.Path, { required: true })
const cachePaths = await utils.resolvePaths(
core
.getInput(Inputs.Path, { required: true })
.split("\n")
.filter(x => x !== "")
);
core.debug(`Cache Path: ${cachePath}`);
const archivePath = path.join(
await utils.createTempDirectory(),
"cache.tgz"
);
core.debug("Cache Paths:");
core.debug(`${JSON.stringify(cachePaths)}`);
const archiveFolder = await utils.createTempDirectory();
const archivePath = path.join(archiveFolder, CacheFilename);
core.debug(`Archive Path: ${archivePath}`);
await createTar(archivePath, cachePath);
await createTar(archiveFolder, cachePaths);
const fileSizeLimit = 5 * 1024 * 1024 * 1024; // 5GB per repo limit
const archiveFileSize = utils.getArchiveFileSize(archivePath);

View File

@ -1,6 +1,9 @@
import { exec } from "@actions/exec";
import * as io from "@actions/io";
import { existsSync } from "fs";
import { existsSync, writeFileSync } from "fs";
import * as path from "path";
import { CacheFilename } from "./constants";
async function getTarPath(): Promise<string> {
// Explicitly use BSD Tar on Windows
@ -14,9 +17,9 @@ async function getTarPath(): Promise<string> {
return await io.which("tar", true);
}
async function execTar(args: string[]): Promise<void> {
async function execTar(args: string[], cwd?: string): Promise<void> {
try {
await exec(`"${await getTarPath()}"`, args);
await exec(`"${await getTarPath()}"`, args, { cwd: cwd });
} catch (error) {
const IS_WINDOWS = process.platform === "win32";
if (IS_WINDOWS) {
@ -28,20 +31,38 @@ async function execTar(args: string[]): Promise<void> {
}
}
export async function extractTar(
archivePath: string,
targetDirectory: string
): Promise<void> {
function getWorkingDirectory(): string {
return process.env["GITHUB_WORKSPACE"] ?? process.cwd();
}
export async function extractTar(archivePath: string): Promise<void> {
// Create directory to extract tar into
await io.mkdirP(targetDirectory);
const args = ["-xz", "-f", archivePath, "-C", targetDirectory];
const workingDirectory = getWorkingDirectory();
await io.mkdirP(workingDirectory);
const args = ["-xz", "-f", archivePath, "-P", "-C", workingDirectory];
await execTar(args);
}
export async function createTar(
archivePath: string,
sourceDirectory: string
archiveFolder: string,
sourceDirectories: string[]
): Promise<void> {
const args = ["-cz", "-f", archivePath, "-C", sourceDirectory, "."];
await execTar(args);
// Write source directories to manifest.txt to avoid command length limits
const manifestFilename = "manifest.txt";
writeFileSync(
path.join(archiveFolder, manifestFilename),
sourceDirectories.join("\n")
);
const workingDirectory = getWorkingDirectory();
const args = [
"-cz",
"-f",
CacheFilename,
"-C",
workingDirectory,
"--files-from",
manifestFilename
];
await execTar(args, archiveFolder);
}

View File

@ -1,7 +1,7 @@
import * as core from "@actions/core";
import * as glob from "@actions/glob";
import * as io from "@actions/io";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import * as util from "util";
import * as uuidV4 from "uuid/v4";
@ -29,6 +29,7 @@ export async function createTempDirectory(): Promise<string> {
}
tempDirectory = path.join(baseLocation, "actions", "temp");
}
const dest = path.join(tempDirectory, uuidV4.default());
await io.mkdirP(dest);
return dest;
@ -83,16 +84,21 @@ export function logWarning(message: string): void {
core.info(`${warningPrefix}${message}`);
}
export function resolvePath(filePath: string): string {
if (filePath[0] === "~") {
const home = os.homedir();
if (!home) {
throw new Error("Unable to resolve `~` to HOME");
}
return path.join(home, filePath.slice(1));
export async function resolvePaths(patterns: string[]): Promise<string[]> {
const paths: string[] = [];
const workspace = process.env["GITHUB_WORKSPACE"] ?? process.cwd();
const globber = await glob.create(patterns.join("\n"), {
implicitDescendants: false
});
for await (const file of globber.globGenerator()) {
const relativeFile = path.relative(workspace, file);
core.debug(`Matched: ${relativeFile}`);
// Paths are made relative so the tar entries are all relative to the root of the workspace.
paths.push(`${relativeFile}`);
}
return path.resolve(filePath);
return paths;
}
export function getSupportedEvents(): string[] {