Compare commits

...

11 Commits

Author SHA1 Message Date
Dave Hadka
2a973a0f4e Add comment for SocketTimeout 2020-05-11 14:32:38 -04:00
Dave Hadka
cbbb8b4d4f Fix lint issue, build .js files 2020-05-11 14:31:13 -04:00
Dave Hadka
5a0add1806 Adds socket timeout and validate file size 2020-05-11 14:28:28 -04:00
Aiqiao Yan
9fe7ad8b07 Use path.sep in path replace 2020-05-11 14:20:33 -04:00
Aiqiao Yan
7c7d003bbb Rebase and rebuild 2020-05-11 14:08:26 -04:00
Aiqiao Yan
96e5a46c57 Fix test 2020-05-11 14:08:26 -04:00
Aiqiao Yan
84e606dfac Fallback to GNU tar if BSD tar is unavailable 2020-05-11 14:08:23 -04:00
Josh Gross
70655ec832 Release v1.1.2 2020-02-05 10:41:57 -05:00
Josh Gross
78a4b2143b Bump version to 1.1.2 2020-02-05 10:40:53 -05:00
Josh Gross
4dc4b4e758 Change name back to Cache 2020-02-05 10:39:52 -05:00
Josh Gross
85aee6a487 Update docs with 5GB limit 2020-02-05 10:33:21 -05:00
10 changed files with 206 additions and 33 deletions

View File

@@ -80,7 +80,7 @@ See [Examples](examples.md) for a list of `actions/cache` implementations for us
## Cache Limits
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

View File

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

View File

@@ -1,4 +1,4 @@
name: 'Cache Artifacts'
name: 'Cache'
description: 'Cache artifacts like dependencies and build outputs to improve workflow execution time'
author: 'GitHub'
inputs:

64
dist/restore/index.js vendored
View File

@@ -1256,6 +1256,7 @@ const fs = __importStar(__webpack_require__(747));
const auth_1 = __webpack_require__(226);
const http_client_1 = __webpack_require__(539);
const utils = __importStar(__webpack_require__(443));
const constants_1 = __webpack_require__(694);
function isSuccessStatusCode(statusCode) {
if (!statusCode) {
return false;
@@ -1339,7 +1340,24 @@ function downloadCache(archiveLocation, archivePath) {
const stream = fs.createWriteStream(archivePath);
const httpClient = new http_client_1.HttpClient("actions/cache");
const downloadResponse = yield httpClient.get(archiveLocation);
// Abort download if no traffic received over the socket.
downloadResponse.message.socket.setTimeout(constants_1.SocketTimeout, () => {
downloadResponse.message.destroy();
core.debug(`Aborting download, socket timed out after ${constants_1.SocketTimeout} ms`);
});
yield 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");
}
});
}
exports.downloadCache = downloadCache;
@@ -1647,6 +1665,10 @@ var __importStar = (this && this.__importStar) || function (mod) {
};
Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(__webpack_require__(470));
<<<<<<< HEAD
=======
const glob = __importStar(__webpack_require__(281));
>>>>>>> 9bb13c7... Fix lint issue, build .js files
const io = __importStar(__webpack_require__(1));
const fs = __importStar(__webpack_require__(747));
const os = __importStar(__webpack_require__(87));
@@ -2022,6 +2044,12 @@ class HttpClientResponse {
this.message.on('data', (chunk) => {
output = Buffer.concat([output, chunk]);
});
this.message.on('aborted', () => {
reject("Request was aborted or closed prematurely");
});
this.message.on('timeout', (socket) => {
reject("Request timed out");
});
this.message.on('end', () => {
resolve(output.toString());
});
@@ -2143,6 +2171,7 @@ class HttpClient {
let response;
while (numTries < maxTries) {
response = await this.requestRaw(info, data);
// Check if it's an authentication challenge
if (response && response.message && response.message.statusCode === HttpCodes.Unauthorized) {
let authenticationHandler;
@@ -2721,6 +2750,10 @@ var Events;
Events["Push"] = "push";
Events["PullRequest"] = "pull_request";
})(Events = exports.Events || (exports.Events = {}));
// 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.
exports.SocketTimeout = 5000;
/***/ }),
@@ -2928,10 +2961,30 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(__webpack_require__(470));
const exec_1 = __webpack_require__(986);
const io = __importStar(__webpack_require__(1));
const fs_1 = __webpack_require__(747);
function getTarPath() {
const path = __importStar(__webpack_require__(622));
const constants_1 = __webpack_require__(694);
function isGnuTar() {
return __awaiter(this, void 0, void 0, function* () {
core.debug("Checking tar --version");
let versionOutput = "";
yield exec_1.exec("tar --version", [], {
ignoreReturnCode: true,
silent: true,
listeners: {
stdout: (data) => (versionOutput += data.toString()),
stderr: (data) => (versionOutput += data.toString())
}
});
core.debug(versionOutput.trim());
return versionOutput.toUpperCase().includes("GNU TAR");
});
}
exports.isGnuTar = isGnuTar;
function getTarPath(args) {
return __awaiter(this, void 0, void 0, function* () {
// Explicitly use BSD Tar on Windows
const IS_WINDOWS = process.platform === "win32";
@@ -2940,6 +2993,9 @@ function getTarPath() {
if (fs_1.existsSync(systemTar)) {
return systemTar;
}
else if (isGnuTar()) {
args.push("--force-local");
}
}
return yield io.which("tar", true);
});
@@ -2951,11 +3007,7 @@ function execTar(args) {
yield exec_1.exec(`"${yield getTarPath()}"`, args);
}
catch (error) {
const IS_WINDOWS = process.platform === "win32";
if (IS_WINDOWS) {
throw new Error(`Tar failed with error: ${(_a = error) === null || _a === void 0 ? void 0 : _a.message}. Ensure BSD tar is installed and on the PATH.`);
}
throw new Error(`Tar failed with error: ${(_b = error) === null || _b === void 0 ? void 0 : _b.message}`);
throw new Error(`Tar failed with error: ${(_a = error) === null || _a === void 0 ? void 0 : _a.message}`);
}
});
}

42
dist/save/index.js vendored
View File

@@ -1339,7 +1339,24 @@ function downloadCache(archiveLocation, archivePath) {
const stream = fs.createWriteStream(archivePath);
const httpClient = new http_client_1.HttpClient("actions/cache");
const downloadResponse = yield httpClient.get(archiveLocation);
// Abort download if no traffic received over the socket.
downloadResponse.message.socket.setTimeout(constants_1.SocketTimeout, () => {
downloadResponse.message.destroy();
core.debug(`Aborting download, socket timed out after ${constants_1.SocketTimeout} ms`);
});
yield 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");
}
});
}
exports.downloadCache = downloadCache;
@@ -1647,6 +1664,10 @@ var __importStar = (this && this.__importStar) || function (mod) {
};
Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(__webpack_require__(470));
<<<<<<< HEAD
=======
const glob = __importStar(__webpack_require__(281));
>>>>>>> 9bb13c7... Fix lint issue, build .js files
const io = __importStar(__webpack_require__(1));
const fs = __importStar(__webpack_require__(747));
const os = __importStar(__webpack_require__(87));
@@ -2022,6 +2043,12 @@ class HttpClientResponse {
this.message.on('data', (chunk) => {
output = Buffer.concat([output, chunk]);
});
this.message.on('aborted', () => {
reject("Request was aborted or closed prematurely");
});
this.message.on('timeout', (socket) => {
reject("Request timed out");
});
this.message.on('end', () => {
resolve(output.toString());
});
@@ -2143,6 +2170,7 @@ class HttpClient {
let response;
while (numTries < maxTries) {
response = await this.requestRaw(info, data);
// Check if it's an authentication challenge
if (response && response.message && response.message.statusCode === HttpCodes.Unauthorized) {
let authenticationHandler;
@@ -2802,6 +2830,10 @@ var Events;
Events["Push"] = "push";
Events["PullRequest"] = "pull_request";
})(Events = exports.Events || (exports.Events = {}));
// 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.
exports.SocketTimeout = 5000;
/***/ }),
@@ -2909,6 +2941,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
const core = __importStar(__webpack_require__(470));
const exec_1 = __webpack_require__(986);
const io = __importStar(__webpack_require__(1));
const fs_1 = __webpack_require__(747);
@@ -2921,6 +2954,9 @@ function getTarPath() {
if (fs_1.existsSync(systemTar)) {
return systemTar;
}
else if (isGnuTar()) {
args.push("--force-local");
}
}
return yield io.which("tar", true);
});
@@ -2932,11 +2968,7 @@ function execTar(args) {
yield exec_1.exec(`"${yield getTarPath()}"`, args);
}
catch (error) {
const IS_WINDOWS = process.platform === "win32";
if (IS_WINDOWS) {
throw new Error(`Tar failed with error: ${(_a = error) === null || _a === void 0 ? void 0 : _a.message}. Ensure BSD tar is installed and on the PATH.`);
}
throw new Error(`Tar failed with error: ${(_b = error) === null || _b === void 0 ? void 0 : _b.message}`);
throw new Error(`Tar failed with error: ${(_a = error) === null || _a === void 0 ? void 0 : _a.message}`);
}
});
}

2
package-lock.json generated
View File

@@ -1,6 +1,6 @@
{
"name": "cache",
"version": "1.1.1",
"version": "1.1.2",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -1,6 +1,6 @@
{
"name": "cache",
"version": "1.1.1",
"version": "1.1.2",
"private": true,
"description": "Cache dependencies and build outputs",
"main": "dist/restore/index.js",

View File

@@ -7,6 +7,8 @@ import {
IRequestOptions,
ITypedResponse
} from "@actions/http-client/interfaces";
import { SocketTimeout } from "./constants";
import {
ArtifactCacheEntry,
CommitCacheRequest,
@@ -123,7 +125,33 @@ export async function downloadCache(
const stream = fs.createWriteStream(archivePath);
const httpClient = new HttpClient("actions/cache");
const downloadResponse = await 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

View File

@@ -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;

View File

@@ -1,14 +1,35 @@
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";
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
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 isGnuTar()) {
args.push("--force-local");
}
}
return await io.which("tar", true);
@@ -16,14 +37,8 @@ async function getTarPath(): Promise<string> {
async function execTar(args: string[]): Promise<void> {
try {
await exec(`"${await getTarPath()}"`, args);
await exec(`"${await getTarPath(args)}"`, args);
} catch (error) {
const IS_WINDOWS = process.platform === "win32";
if (IS_WINDOWS) {
throw new Error(
`Tar failed with error: ${error?.message}. Ensure BSD tar is installed and on the PATH.`
);
}
throw new Error(`Tar failed with error: ${error?.message}`);
}
}
@@ -34,7 +49,13 @@ export async function extractTar(
): Promise<void> {
// Create directory to extract tar into
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);
}
@@ -42,6 +63,13 @@ export async function createTar(
archivePath: string,
sourceDirectory: string
): 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);
}