Compare commits
No commits in common. "v1" and "v1.1.2" have entirely different histories.
111
.github/workflows/workflow.yml
vendored
111
.github/workflows/workflow.yml
vendored
@ -4,130 +4,51 @@ 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:
|
||||||
# Build and unit test
|
test:
|
||||||
build:
|
name: Test on ${{ matrix.os }}
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, windows-latest, macOS-latest]
|
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- uses: actions/checkout@v1
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Setup Node.js
|
- uses: actions/setup-node@v1
|
||||||
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: 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
|
|
||||||
|
|||||||
@ -1,144 +0,0 @@
|
|||||||
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
|
|
||||||
);
|
|
||||||
});
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@ -2,8 +2,6 @@ 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");
|
||||||
|
|
||||||
@ -13,19 +11,17 @@ beforeAll(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("extract BSD tar", async () => {
|
test("extract 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 IS_WINDOWS = process.platform === "win32";
|
const archivePath = "cache.tar";
|
||||||
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";
|
||||||
@ -33,37 +29,13 @@ test("extract BSD tar", async () => {
|
|||||||
expect(execMock).toHaveBeenCalledWith(`"${tarPath}"`, [
|
expect(execMock).toHaveBeenCalledWith(`"${tarPath}"`, [
|
||||||
"-xz",
|
"-xz",
|
||||||
"-f",
|
"-f",
|
||||||
IS_WINDOWS ? archivePath.replace(/\\/g, "/") : archivePath,
|
archivePath,
|
||||||
"-C",
|
"-C",
|
||||||
IS_WINDOWS ? targetDirectory?.replace(/\\/g, "/") : targetDirectory
|
targetDirectory
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("extract GNU tar", async () => {
|
test("create 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";
|
||||||
@ -78,9 +50,9 @@ test("create BSD tar", async () => {
|
|||||||
expect(execMock).toHaveBeenCalledWith(`"${tarPath}"`, [
|
expect(execMock).toHaveBeenCalledWith(`"${tarPath}"`, [
|
||||||
"-cz",
|
"-cz",
|
||||||
"-f",
|
"-f",
|
||||||
IS_WINDOWS ? archivePath.replace(/\\/g, "/") : archivePath,
|
archivePath,
|
||||||
"-C",
|
"-C",
|
||||||
IS_WINDOWS ? sourceDirectory?.replace(/\\/g, "/") : sourceDirectory,
|
sourceDirectory,
|
||||||
"."
|
"."
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,30 +0,0 @@
|
|||||||
#!/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
|
|
||||||
2396
dist/restore/index.js
vendored
2396
dist/restore/index.js
vendored
File diff suppressed because it is too large
Load Diff
2396
dist/save/index.js
vendored
2396
dist/save/index.js
vendored
File diff suppressed because it is too large
Load Diff
27
package-lock.json
generated
27
package-lock.json
generated
@ -1,32 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "cache",
|
"name": "cache",
|
||||||
"version": "1.2.0",
|
"version": "1.1.2",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": {
|
"@actions/core": {
|
||||||
"version": "1.10.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.0.tgz",
|
||||||
"integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
|
"integrity": "sha512-ZKdyhlSlyz38S6YFfPnyNgCDZuAF2T0Qv5eHflNWytPS8Qjvz39bZFMry9Bb/dpSnqWcNeav5yM2CTYpJeY+Dw=="
|
||||||
"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": {
|
"@actions/exec": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cache",
|
"name": "cache",
|
||||||
"version": "1.2.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",
|
||||||
@ -24,7 +24,7 @@
|
|||||||
"author": "GitHub",
|
"author": "GitHub",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.10.0",
|
"@actions/core": "^1.2.0",
|
||||||
"@actions/exec": "^1.0.1",
|
"@actions/exec": "^1.0.1",
|
||||||
"@actions/http-client": "^1.0.6",
|
"@actions/http-client": "^1.0.6",
|
||||||
"@actions/io": "^1.0.1",
|
"@actions/io": "^1.0.1",
|
||||||
|
|||||||
@ -1,16 +1,12 @@
|
|||||||
import * as core from "@actions/core";
|
import * as core from "@actions/core";
|
||||||
import { HttpClient, HttpCodes } from "@actions/http-client";
|
import * as fs from "fs";
|
||||||
import { BearerCredentialHandler } from "@actions/http-client/auth";
|
import { BearerCredentialHandler } from "@actions/http-client/auth";
|
||||||
|
import { HttpClient, HttpCodes } from "@actions/http-client";
|
||||||
import {
|
import {
|
||||||
IHttpClientResponse,
|
IHttpClientResponse,
|
||||||
IRequestOptions,
|
IRequestOptions,
|
||||||
ITypedResponse
|
ITypedResponse
|
||||||
} from "@actions/http-client/interfaces";
|
} from "@actions/http-client/interfaces";
|
||||||
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,
|
||||||
@ -26,13 +22,6 @@ function isSuccessStatusCode(statusCode?: number): boolean {
|
|||||||
return statusCode >= 200 && statusCode < 300;
|
return statusCode >= 200 && statusCode < 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isServerErrorStatusCode(statusCode?: number): boolean {
|
|
||||||
if (!statusCode) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return statusCode >= 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRetryableStatusCode(statusCode?: number): boolean {
|
function isRetryableStatusCode(statusCode?: number): boolean {
|
||||||
if (!statusCode) {
|
if (!statusCode) {
|
||||||
return false;
|
return false;
|
||||||
@ -88,83 +77,14 @@ function createHttpClient(): HttpClient {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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 httpClient = createHttpClient();
|
const httpClient = createHttpClient();
|
||||||
const resource = `cache?keys=${encodeURIComponent(keys.join(","))}`;
|
const resource = `cache?keys=${encodeURIComponent(keys.join(","))}`;
|
||||||
|
|
||||||
const response = await retryTypedResponse("getCacheEntry", () =>
|
const response = await httpClient.getJson<ArtifactCacheEntry>(
|
||||||
httpClient.getJson<ArtifactCacheEntry>(getCacheApiUrl(resource))
|
getCacheApiUrl(resource)
|
||||||
);
|
);
|
||||||
if (response.statusCode === 204) {
|
if (response.statusCode === 204) {
|
||||||
return null;
|
return null;
|
||||||
@ -187,10 +107,13 @@ export async function getCacheEntry(
|
|||||||
|
|
||||||
async function pipeResponseToStream(
|
async function pipeResponseToStream(
|
||||||
response: IHttpClientResponse,
|
response: IHttpClientResponse,
|
||||||
output: NodeJS.WritableStream
|
stream: NodeJS.WritableStream
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const pipeline = util.promisify(stream.pipeline);
|
return new Promise(resolve => {
|
||||||
await pipeline(response.message, output);
|
response.message.pipe(stream).on("close", () => {
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadCache(
|
export async function downloadCache(
|
||||||
@ -199,37 +122,8 @@ 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 retryHttpClientResponse(
|
const downloadResponse = await httpClient.get(archiveLocation);
|
||||||
"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
|
||||||
@ -239,11 +133,9 @@ export async function reserveCache(key: string): Promise<number> {
|
|||||||
const reserveCacheRequest: ReserveCacheRequest = {
|
const reserveCacheRequest: ReserveCacheRequest = {
|
||||||
key
|
key
|
||||||
};
|
};
|
||||||
const response = await retryTypedResponse("reserveCache", () =>
|
const response = await httpClient.postJson<ReserveCacheResponse>(
|
||||||
httpClient.postJson<ReserveCacheResponse>(
|
|
||||||
getCacheApiUrl("caches"),
|
getCacheApiUrl("caches"),
|
||||||
reserveCacheRequest
|
reserveCacheRequest
|
||||||
)
|
|
||||||
);
|
);
|
||||||
return response?.result?.cacheId ?? -1;
|
return response?.result?.cacheId ?? -1;
|
||||||
}
|
}
|
||||||
@ -260,7 +152,7 @@ function getContentRange(start: number, end: number): string {
|
|||||||
async function uploadChunk(
|
async function uploadChunk(
|
||||||
httpClient: HttpClient,
|
httpClient: HttpClient,
|
||||||
resourceUrl: string,
|
resourceUrl: string,
|
||||||
openStream: () => NodeJS.ReadableStream,
|
data: NodeJS.ReadableStream,
|
||||||
start: number,
|
start: number,
|
||||||
end: number
|
end: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
@ -277,15 +169,32 @@ async function uploadChunk(
|
|||||||
"Content-Range": getContentRange(start, end)
|
"Content-Range": getContentRange(start, end)
|
||||||
};
|
};
|
||||||
|
|
||||||
await retryHttpClientResponse(
|
const uploadChunkRequest = async (): Promise<IHttpClientResponse> => {
|
||||||
`uploadChunk (start: ${start}, end: ${end})`,
|
return await httpClient.sendStream(
|
||||||
() =>
|
|
||||||
httpClient.sendStream(
|
|
||||||
"PATCH",
|
"PATCH",
|
||||||
resourceUrl,
|
resourceUrl,
|
||||||
openStream(),
|
data,
|
||||||
additionalHeaders
|
additionalHeaders
|
||||||
)
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await uploadChunkRequest();
|
||||||
|
if (isSuccessStatusCode(response.message.statusCode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRetryableStatusCode(response.message.statusCode)) {
|
||||||
|
core.debug(
|
||||||
|
`Received ${response.message.statusCode}, retrying chunk at offset ${start}.`
|
||||||
|
);
|
||||||
|
const retryResponse = await uploadChunkRequest();
|
||||||
|
if (isSuccessStatusCode(retryResponse.message.statusCode)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Cache service responded with ${response.message.statusCode} during chunk upload.`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -327,23 +236,17 @@ 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 => {
|
|
||||||
throw new Error(
|
await uploadChunk(
|
||||||
`Cache upload failed because file read failed with ${error.Message}`
|
httpClient,
|
||||||
);
|
resourceUrl,
|
||||||
}),
|
chunk,
|
||||||
start,
|
start,
|
||||||
end
|
end
|
||||||
);
|
);
|
||||||
@ -362,11 +265,9 @@ async function commitCache(
|
|||||||
filesize: number
|
filesize: number
|
||||||
): Promise<ITypedResponse<null>> {
|
): Promise<ITypedResponse<null>> {
|
||||||
const commitCacheRequest: CommitCacheRequest = { size: filesize };
|
const commitCacheRequest: CommitCacheRequest = { size: filesize };
|
||||||
return await retryTypedResponse("commitCache", () =>
|
return await httpClient.postJson<null>(
|
||||||
httpClient.postJson<null>(
|
|
||||||
getCacheApiUrl(`caches/${cacheId.toString()}`),
|
getCacheApiUrl(`caches/${cacheId.toString()}`),
|
||||||
commitCacheRequest
|
commitCacheRequest
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,8 +18,3 @@ 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;
|
|
||||||
|
|||||||
49
src/tar.ts
49
src/tar.ts
@ -1,36 +1,14 @@
|
|||||||
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";
|
|
||||||
|
|
||||||
export async function isGnuTar(): Promise<boolean> {
|
async function getTarPath(): Promise<string> {
|
||||||
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);
|
||||||
@ -38,8 +16,14 @@ async function getTarPath(args: string[]): Promise<string> {
|
|||||||
|
|
||||||
async function execTar(args: string[]): Promise<void> {
|
async function execTar(args: string[]): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await exec(`"${await getTarPath(args)}"`, args);
|
await exec(`"${await getTarPath()}"`, 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}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -50,13 +34,7 @@ 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 = [
|
const args = ["-xz", "-f", archivePath, "-C", targetDirectory];
|
||||||
"-xz",
|
|
||||||
"-f",
|
|
||||||
archivePath.replace(new RegExp("\\" + path.sep, "g"), "/"),
|
|
||||||
"-C",
|
|
||||||
targetDirectory.replace(new RegExp("\\" + path.sep, "g"), "/")
|
|
||||||
];
|
|
||||||
await execTar(args);
|
await execTar(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,13 +42,6 @@ export async function createTar(
|
|||||||
archivePath: string,
|
archivePath: string,
|
||||||
sourceDirectory: string
|
sourceDirectory: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const args = [
|
const args = ["-cz", "-f", archivePath, "-C", sourceDirectory, "."];
|
||||||
"-cz",
|
|
||||||
"-f",
|
|
||||||
archivePath.replace(new RegExp("\\" + path.sep, "g"), "/"),
|
|
||||||
"-C",
|
|
||||||
sourceDirectory.replace(new RegExp("\\" + path.sep, "g"), "/"),
|
|
||||||
"."
|
|
||||||
];
|
|
||||||
await execTar(args);
|
await execTar(args);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user