mirror of
				https://gitea.com/actions/cache.git
				synced 2025-11-02 07:47:08 +00:00 
			
		
		
		
	Compare commits
	
		
			29 Commits
		
	
	
		
			v4.0.2
			...
			releases/v
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					f5ce41475b | ||
| 
						 | 
					68fa0a8d81 | ||
| 
						 | 
					56ec64e417 | ||
| 
						 | 
					efbc4e162b | ||
| 
						 | 
					d9747005de | ||
| 
						 | 
					3f662ca624 | ||
| 
						 | 
					0232e3178d | ||
| 
						 | 
					ee7a57c615 | ||
| 
						 | 
					da9f90cb83 | ||
| 
						 | 
					ec7f7ebd08 | ||
| 
						 | 
					2a973a0f4e | ||
| 
						 | 
					cbbb8b4d4f | ||
| 
						 | 
					5a0add1806 | ||
| 
						 | 
					9fe7ad8b07 | ||
| 
						 | 
					7c7d003bbb | ||
| 
						 | 
					96e5a46c57 | ||
| 
						 | 
					84e606dfac | ||
| 
						 | 
					70655ec832 | ||
| 
						 | 
					fe1055e9d1 | ||
| 
						 | 
					a505c2e7a6 | ||
| 
						 | 
					10a14413e7 | ||
| 
						 | 
					cf4f44db70 | ||
| 
						 | 
					4c4974aff1 | ||
| 
						 | 
					cffae9552b | ||
| 
						 | 
					44543250bd | ||
| 
						 | 
					6491e51b66 | ||
| 
						 | 
					86dff562ab | ||
| 
						 | 
					0f810ad45a | ||
| 
						 | 
					9d8c7b4041 | 
							
								
								
									
										111
									
								
								.github/workflows/workflow.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										111
									
								
								.github/workflows/workflow.yml
									
									
									
									
										vendored
									
									
								
							@@ -4,51 +4,130 @@ on:
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches:
 | 
			
		||||
      - master
 | 
			
		||||
      - releases/**
 | 
			
		||||
    paths-ignore:
 | 
			
		||||
      - '**.md'
 | 
			
		||||
  push:
 | 
			
		||||
    branches:
 | 
			
		||||
      - master
 | 
			
		||||
      - releases/**
 | 
			
		||||
    paths-ignore:
 | 
			
		||||
      - '**.md'
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  test:
 | 
			
		||||
    name: Test on ${{ matrix.os }}
 | 
			
		||||
 | 
			
		||||
  # Build and unit test
 | 
			
		||||
  build:
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        os: [ubuntu-latest, windows-latest, macOS-latest]
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
 | 
			
		||||
    runs-on: ${{ matrix.os }}
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
    - uses: actions/checkout@v1
 | 
			
		||||
 | 
			
		||||
    - uses: actions/setup-node@v1
 | 
			
		||||
    - name: Checkout
 | 
			
		||||
      uses: actions/checkout@v2
 | 
			
		||||
    - name: Setup Node.js
 | 
			
		||||
      uses: actions/setup-node@v1
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: '12.x'
 | 
			
		||||
 | 
			
		||||
    - name: Get npm cache directory
 | 
			
		||||
    - name: Determine npm cache directory
 | 
			
		||||
      id: npm-cache
 | 
			
		||||
      run: |
 | 
			
		||||
        echo "::set-output name=dir::$(npm config get cache)"
 | 
			
		||||
 | 
			
		||||
    - uses: actions/cache@v1
 | 
			
		||||
    - name: Restore npm cache
 | 
			
		||||
      uses: actions/cache@v1
 | 
			
		||||
      with:
 | 
			
		||||
        path: ${{ steps.npm-cache.outputs.dir }}
 | 
			
		||||
        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
 | 
			
		||||
        restore-keys: |
 | 
			
		||||
          ${{ runner.os }}-node-
 | 
			
		||||
 | 
			
		||||
    - run: npm ci
 | 
			
		||||
 | 
			
		||||
    - name: Prettier Format Check
 | 
			
		||||
      run: npm run format-check
 | 
			
		||||
 | 
			
		||||
    - name: ESLint Check
 | 
			
		||||
      run: npm run lint
 | 
			
		||||
 | 
			
		||||
    - name: Build & Test
 | 
			
		||||
      run: npm run test
 | 
			
		||||
 | 
			
		||||
  # End to end save and restore
 | 
			
		||||
  test-save:
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        os: [ubuntu-latest, windows-latest, macOS-latest]
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
    runs-on: ${{ matrix.os }}
 | 
			
		||||
    steps:
 | 
			
		||||
    - name: Checkout
 | 
			
		||||
      uses: actions/checkout@v2
 | 
			
		||||
    - name: Generate files
 | 
			
		||||
      shell: bash
 | 
			
		||||
      run: __tests__/create-cache-files.sh ${{ runner.os }}
 | 
			
		||||
    - name: Save cache
 | 
			
		||||
      uses: ./
 | 
			
		||||
      with:
 | 
			
		||||
        key: test-${{ runner.os }}-${{ github.run_id }}
 | 
			
		||||
        path: test-cache
 | 
			
		||||
  test-restore:
 | 
			
		||||
    needs: test-save
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        os: [ubuntu-latest, windows-latest, macOS-latest]
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
    runs-on: ${{ matrix.os }}
 | 
			
		||||
    steps:
 | 
			
		||||
    - name: Checkout
 | 
			
		||||
      uses: actions/checkout@v2
 | 
			
		||||
    - name: Restore cache
 | 
			
		||||
      uses: ./
 | 
			
		||||
      with:
 | 
			
		||||
        key: test-${{ runner.os }}-${{ github.run_id }}
 | 
			
		||||
        path: test-cache
 | 
			
		||||
    - name: Verify cache
 | 
			
		||||
      shell: bash
 | 
			
		||||
      run: __tests__/verify-cache-files.sh ${{ runner.os }}
 | 
			
		||||
 | 
			
		||||
  # End to end with proxy
 | 
			
		||||
  test-proxy-save:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    container:
 | 
			
		||||
      image: ubuntu:latest
 | 
			
		||||
      options: --dns 127.0.0.1
 | 
			
		||||
    services:
 | 
			
		||||
      squid-proxy:
 | 
			
		||||
        image: ubuntu/squid:latest
 | 
			
		||||
        ports:
 | 
			
		||||
          - 3128:3128
 | 
			
		||||
    env:
 | 
			
		||||
      https_proxy: http://squid-proxy:3128
 | 
			
		||||
    steps:
 | 
			
		||||
    - name: Checkout
 | 
			
		||||
      uses: actions/checkout@v2
 | 
			
		||||
    - name: Generate files
 | 
			
		||||
      run: __tests__/create-cache-files.sh proxy
 | 
			
		||||
    - name: Save cache
 | 
			
		||||
      uses: ./
 | 
			
		||||
      with:
 | 
			
		||||
        key: test-proxy-${{ github.run_id }}
 | 
			
		||||
        path: test-cache
 | 
			
		||||
  test-proxy-restore:
 | 
			
		||||
    needs: test-proxy-save
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    container:
 | 
			
		||||
      image: ubuntu:latest
 | 
			
		||||
      options: --dns 127.0.0.1
 | 
			
		||||
    services:
 | 
			
		||||
      squid-proxy:
 | 
			
		||||
        image: ubuntu/squid:latest
 | 
			
		||||
        ports:
 | 
			
		||||
          - 3128:3128
 | 
			
		||||
    env:
 | 
			
		||||
      https_proxy: http://squid-proxy:3128
 | 
			
		||||
    steps:
 | 
			
		||||
    - name: Checkout
 | 
			
		||||
      uses: actions/checkout@v2
 | 
			
		||||
    - name: Restore cache
 | 
			
		||||
      uses: ./
 | 
			
		||||
      with:
 | 
			
		||||
        key: test-proxy-${{ github.run_id }}
 | 
			
		||||
        path: test-cache
 | 
			
		||||
    - name: Verify cache
 | 
			
		||||
      run: __tests__/verify-cache-files.sh proxy
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										144
									
								
								__tests__/cacheHttpsClient.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								__tests__/cacheHttpsClient.test.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,144 @@
 | 
			
		||||
import { retry } from "../src/cacheHttpClient";
 | 
			
		||||
import * as testUtils from "../src/utils/testUtils";
 | 
			
		||||
 | 
			
		||||
afterEach(() => {
 | 
			
		||||
    testUtils.clearInputs();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
interface TestResponse {
 | 
			
		||||
    statusCode: number;
 | 
			
		||||
    result: string | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function handleResponse(
 | 
			
		||||
    response: TestResponse | undefined
 | 
			
		||||
): Promise<TestResponse> {
 | 
			
		||||
    if (!response) {
 | 
			
		||||
        fail("Retry method called too many times");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (response.statusCode === 999) {
 | 
			
		||||
        throw Error("Test Error");
 | 
			
		||||
    } else {
 | 
			
		||||
        return Promise.resolve(response);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function testRetryExpectingResult(
 | 
			
		||||
    responses: Array<TestResponse>,
 | 
			
		||||
    expectedResult: string | null
 | 
			
		||||
): Promise<void> {
 | 
			
		||||
    responses = responses.reverse(); // Reverse responses since we pop from end
 | 
			
		||||
 | 
			
		||||
    const actualResult = await retry(
 | 
			
		||||
        "test",
 | 
			
		||||
        () => handleResponse(responses.pop()),
 | 
			
		||||
        (response: TestResponse) => response.statusCode
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    expect(actualResult.result).toEqual(expectedResult);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function testRetryExpectingError(
 | 
			
		||||
    responses: Array<TestResponse>
 | 
			
		||||
): Promise<void> {
 | 
			
		||||
    responses = responses.reverse(); // Reverse responses since we pop from end
 | 
			
		||||
 | 
			
		||||
    expect(
 | 
			
		||||
        retry(
 | 
			
		||||
            "test",
 | 
			
		||||
            () => handleResponse(responses.pop()),
 | 
			
		||||
            (response: TestResponse) => response.statusCode
 | 
			
		||||
        )
 | 
			
		||||
    ).rejects.toBeInstanceOf(Error);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
test("retry works on successful response", async () => {
 | 
			
		||||
    await testRetryExpectingResult(
 | 
			
		||||
        [
 | 
			
		||||
            {
 | 
			
		||||
                statusCode: 200,
 | 
			
		||||
                result: "Ok"
 | 
			
		||||
            }
 | 
			
		||||
        ],
 | 
			
		||||
        "Ok"
 | 
			
		||||
    );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("retry works after retryable status code", async () => {
 | 
			
		||||
    await testRetryExpectingResult(
 | 
			
		||||
        [
 | 
			
		||||
            {
 | 
			
		||||
                statusCode: 503,
 | 
			
		||||
                result: null
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                statusCode: 200,
 | 
			
		||||
                result: "Ok"
 | 
			
		||||
            }
 | 
			
		||||
        ],
 | 
			
		||||
        "Ok"
 | 
			
		||||
    );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("retry fails after exhausting retries", async () => {
 | 
			
		||||
    await testRetryExpectingError([
 | 
			
		||||
        {
 | 
			
		||||
            statusCode: 503,
 | 
			
		||||
            result: null
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            statusCode: 503,
 | 
			
		||||
            result: null
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            statusCode: 200,
 | 
			
		||||
            result: "Ok"
 | 
			
		||||
        }
 | 
			
		||||
    ]);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("retry fails after non-retryable status code", async () => {
 | 
			
		||||
    await testRetryExpectingError([
 | 
			
		||||
        {
 | 
			
		||||
            statusCode: 500,
 | 
			
		||||
            result: null
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            statusCode: 200,
 | 
			
		||||
            result: "Ok"
 | 
			
		||||
        }
 | 
			
		||||
    ]);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("retry works after error", async () => {
 | 
			
		||||
    await testRetryExpectingResult(
 | 
			
		||||
        [
 | 
			
		||||
            {
 | 
			
		||||
                statusCode: 999,
 | 
			
		||||
                result: null
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                statusCode: 200,
 | 
			
		||||
                result: "Ok"
 | 
			
		||||
            }
 | 
			
		||||
        ],
 | 
			
		||||
        "Ok"
 | 
			
		||||
    );
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("retry returns after client error", async () => {
 | 
			
		||||
    await testRetryExpectingResult(
 | 
			
		||||
        [
 | 
			
		||||
            {
 | 
			
		||||
                statusCode: 400,
 | 
			
		||||
                result: null
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
                statusCode: 200,
 | 
			
		||||
                result: "Ok"
 | 
			
		||||
            }
 | 
			
		||||
        ],
 | 
			
		||||
        null
 | 
			
		||||
    );
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										11
									
								
								__tests__/create-cache-files.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										11
									
								
								__tests__/create-cache-files.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
 | 
			
		||||
# Validate args
 | 
			
		||||
prefix="$1"
 | 
			
		||||
if [ -z "$prefix" ]; then
 | 
			
		||||
  echo "Must supply prefix argument"
 | 
			
		||||
  exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
mkdir test-cache
 | 
			
		||||
echo "$prefix $GITHUB_RUN_ID" > test-cache/test-file.txt
 | 
			
		||||
@@ -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(1);
 | 
			
		||||
        expect(execMock).toHaveBeenLastCalledWith(`"tar"`, [
 | 
			
		||||
            "-xz",
 | 
			
		||||
            "-f",
 | 
			
		||||
            archivePath.replace(/\\/g, "/"),
 | 
			
		||||
            "-C",
 | 
			
		||||
            targetDirectory?.replace(/\\/g, "/"),
 | 
			
		||||
            "--force-local"
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
test("create BSD tar", async () => {
 | 
			
		||||
    const execMock = jest.spyOn(exec, "exec");
 | 
			
		||||
 | 
			
		||||
    const archivePath = "cache.tar";
 | 
			
		||||
@@ -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,
 | 
			
		||||
        "."
 | 
			
		||||
    ]);
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										30
									
								
								__tests__/verify-cache-files.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										30
									
								
								__tests__/verify-cache-files.sh
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
 | 
			
		||||
# Validate args
 | 
			
		||||
prefix="$1"
 | 
			
		||||
if [ -z "$prefix" ]; then
 | 
			
		||||
  echo "Must supply prefix argument"
 | 
			
		||||
  exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# Sanity check GITHUB_RUN_ID defined
 | 
			
		||||
if [ -z "$GITHUB_RUN_ID" ]; then
 | 
			
		||||
  echo "GITHUB_RUN_ID not defined"
 | 
			
		||||
  exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# Verify file exists
 | 
			
		||||
file="test-cache/test-file.txt"
 | 
			
		||||
echo "Checking for $file"
 | 
			
		||||
if [ ! -e $file ]; then
 | 
			
		||||
  echo "File does not exist"
 | 
			
		||||
  exit 1
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
# Verify file content
 | 
			
		||||
content="$(cat $file)"
 | 
			
		||||
echo "File content:\n$content"
 | 
			
		||||
if [ -z "$(echo $content | grep --fixed-strings "$prefix $GITHUB_RUN_ID")" ]; then
 | 
			
		||||
  echo "Unexpected file content"
 | 
			
		||||
  exit 1
 | 
			
		||||
fi
 | 
			
		||||
							
								
								
									
										5337
									
								
								dist/restore/index.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5337
									
								
								dist/restore/index.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										5318
									
								
								dist/save/index.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5318
									
								
								dist/save/index.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										27
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										27
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -1,13 +1,32 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "cache",
 | 
			
		||||
  "version": "1.1.2",
 | 
			
		||||
  "version": "1.2.0",
 | 
			
		||||
  "lockfileVersion": 1,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@actions/core": {
 | 
			
		||||
      "version": "1.2.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.0.tgz",
 | 
			
		||||
      "integrity": "sha512-ZKdyhlSlyz38S6YFfPnyNgCDZuAF2T0Qv5eHflNWytPS8Qjvz39bZFMry9Bb/dpSnqWcNeav5yM2CTYpJeY+Dw=="
 | 
			
		||||
      "version": "1.10.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz",
 | 
			
		||||
      "integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==",
 | 
			
		||||
      "requires": {
 | 
			
		||||
        "@actions/http-client": "^2.0.1",
 | 
			
		||||
        "uuid": "^8.3.2"
 | 
			
		||||
      },
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@actions/http-client": {
 | 
			
		||||
          "version": "2.1.0",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.1.0.tgz",
 | 
			
		||||
          "integrity": "sha512-BonhODnXr3amchh4qkmjPMUO8mFi/zLaaCeCAJZqch8iQqyDnVIkySjB38VHAC8IJ+bnlgfOqlhpyCUZHlQsqw==",
 | 
			
		||||
          "requires": {
 | 
			
		||||
            "tunnel": "^0.0.6"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "uuid": {
 | 
			
		||||
          "version": "8.3.2",
 | 
			
		||||
          "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
 | 
			
		||||
          "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "@actions/exec": {
 | 
			
		||||
      "version": "1.0.1",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "cache",
 | 
			
		||||
  "version": "1.1.2",
 | 
			
		||||
  "version": "1.2.0",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "description": "Cache dependencies and build outputs",
 | 
			
		||||
  "main": "dist/restore/index.js",
 | 
			
		||||
@@ -24,7 +24,7 @@
 | 
			
		||||
  "author": "GitHub",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@actions/core": "^1.2.0",
 | 
			
		||||
    "@actions/core": "^1.10.0",
 | 
			
		||||
    "@actions/exec": "^1.0.1",
 | 
			
		||||
    "@actions/http-client": "^1.0.6",
 | 
			
		||||
    "@actions/io": "^1.0.1",
 | 
			
		||||
 
 | 
			
		||||
@@ -1,12 +1,16 @@
 | 
			
		||||
import * as core from "@actions/core";
 | 
			
		||||
import * as fs from "fs";
 | 
			
		||||
import { BearerCredentialHandler } from "@actions/http-client/auth";
 | 
			
		||||
import { HttpClient, HttpCodes } from "@actions/http-client";
 | 
			
		||||
import { BearerCredentialHandler } from "@actions/http-client/auth";
 | 
			
		||||
import {
 | 
			
		||||
    IHttpClientResponse,
 | 
			
		||||
    IRequestOptions,
 | 
			
		||||
    ITypedResponse
 | 
			
		||||
} from "@actions/http-client/interfaces";
 | 
			
		||||
import * as fs from "fs";
 | 
			
		||||
import * as stream from "stream";
 | 
			
		||||
import * as util from "util";
 | 
			
		||||
 | 
			
		||||
import { SocketTimeout } from "./constants";
 | 
			
		||||
import {
 | 
			
		||||
    ArtifactCacheEntry,
 | 
			
		||||
    CommitCacheRequest,
 | 
			
		||||
@@ -22,6 +26,13 @@ function isSuccessStatusCode(statusCode?: number): boolean {
 | 
			
		||||
    return statusCode >= 200 && statusCode < 300;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isServerErrorStatusCode(statusCode?: number): boolean {
 | 
			
		||||
    if (!statusCode) {
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
    return statusCode >= 500;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isRetryableStatusCode(statusCode?: number): boolean {
 | 
			
		||||
    if (!statusCode) {
 | 
			
		||||
        return false;
 | 
			
		||||
@@ -77,14 +88,83 @@ 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(
 | 
			
		||||
    keys: string[]
 | 
			
		||||
): Promise<ArtifactCacheEntry | null> {
 | 
			
		||||
    const httpClient = createHttpClient();
 | 
			
		||||
    const resource = `cache?keys=${encodeURIComponent(keys.join(","))}`;
 | 
			
		||||
 | 
			
		||||
    const response = await httpClient.getJson<ArtifactCacheEntry>(
 | 
			
		||||
        getCacheApiUrl(resource)
 | 
			
		||||
    const response = await retryTypedResponse("getCacheEntry", () =>
 | 
			
		||||
        httpClient.getJson<ArtifactCacheEntry>(getCacheApiUrl(resource))
 | 
			
		||||
    );
 | 
			
		||||
    if (response.statusCode === 204) {
 | 
			
		||||
        return null;
 | 
			
		||||
@@ -107,13 +187,10 @@ export async function getCacheEntry(
 | 
			
		||||
 | 
			
		||||
async function pipeResponseToStream(
 | 
			
		||||
    response: IHttpClientResponse,
 | 
			
		||||
    stream: NodeJS.WritableStream
 | 
			
		||||
    output: NodeJS.WritableStream
 | 
			
		||||
): Promise<void> {
 | 
			
		||||
    return new Promise(resolve => {
 | 
			
		||||
        response.message.pipe(stream).on("close", () => {
 | 
			
		||||
            resolve();
 | 
			
		||||
        });
 | 
			
		||||
    });
 | 
			
		||||
    const pipeline = util.promisify(stream.pipeline);
 | 
			
		||||
    await pipeline(response.message, output);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function downloadCache(
 | 
			
		||||
@@ -122,8 +199,37 @@ export async function downloadCache(
 | 
			
		||||
): Promise<void> {
 | 
			
		||||
    const stream = fs.createWriteStream(archivePath);
 | 
			
		||||
    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);
 | 
			
		||||
 | 
			
		||||
    // 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
 | 
			
		||||
@@ -133,9 +239,11 @@ export async function reserveCache(key: string): Promise<number> {
 | 
			
		||||
    const reserveCacheRequest: ReserveCacheRequest = {
 | 
			
		||||
        key
 | 
			
		||||
    };
 | 
			
		||||
    const response = await httpClient.postJson<ReserveCacheResponse>(
 | 
			
		||||
        getCacheApiUrl("caches"),
 | 
			
		||||
        reserveCacheRequest
 | 
			
		||||
    const response = await retryTypedResponse("reserveCache", () =>
 | 
			
		||||
        httpClient.postJson<ReserveCacheResponse>(
 | 
			
		||||
            getCacheApiUrl("caches"),
 | 
			
		||||
            reserveCacheRequest
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
    return response?.result?.cacheId ?? -1;
 | 
			
		||||
}
 | 
			
		||||
@@ -152,7 +260,7 @@ function getContentRange(start: number, end: number): string {
 | 
			
		||||
async function uploadChunk(
 | 
			
		||||
    httpClient: HttpClient,
 | 
			
		||||
    resourceUrl: string,
 | 
			
		||||
    data: NodeJS.ReadableStream,
 | 
			
		||||
    openStream: () => NodeJS.ReadableStream,
 | 
			
		||||
    start: number,
 | 
			
		||||
    end: number
 | 
			
		||||
): Promise<void> {
 | 
			
		||||
@@ -169,32 +277,15 @@ async function uploadChunk(
 | 
			
		||||
        "Content-Range": getContentRange(start, end)
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const uploadChunkRequest = async (): Promise<IHttpClientResponse> => {
 | 
			
		||||
        return await httpClient.sendStream(
 | 
			
		||||
            "PATCH",
 | 
			
		||||
            resourceUrl,
 | 
			
		||||
            data,
 | 
			
		||||
            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.`
 | 
			
		||||
    await retryHttpClientResponse(
 | 
			
		||||
        `uploadChunk (start: ${start}, end: ${end})`,
 | 
			
		||||
        () =>
 | 
			
		||||
            httpClient.sendStream(
 | 
			
		||||
                "PATCH",
 | 
			
		||||
                resourceUrl,
 | 
			
		||||
                openStream(),
 | 
			
		||||
                additionalHeaders
 | 
			
		||||
            )
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -236,17 +327,23 @@ async function uploadFile(
 | 
			
		||||
                    const start = offset;
 | 
			
		||||
                    const end = offset + chunkSize - 1;
 | 
			
		||||
                    offset += MAX_CHUNK_SIZE;
 | 
			
		||||
                    const chunk = fs.createReadStream(archivePath, {
 | 
			
		||||
                        fd,
 | 
			
		||||
                        start,
 | 
			
		||||
                        end,
 | 
			
		||||
                        autoClose: false
 | 
			
		||||
                    });
 | 
			
		||||
 | 
			
		||||
                    await uploadChunk(
 | 
			
		||||
                        httpClient,
 | 
			
		||||
                        resourceUrl,
 | 
			
		||||
                        chunk,
 | 
			
		||||
                        () =>
 | 
			
		||||
                            fs
 | 
			
		||||
                                .createReadStream(archivePath, {
 | 
			
		||||
                                    fd,
 | 
			
		||||
                                    start,
 | 
			
		||||
                                    end,
 | 
			
		||||
                                    autoClose: false
 | 
			
		||||
                                })
 | 
			
		||||
                                .on("error", error => {
 | 
			
		||||
                                    throw new Error(
 | 
			
		||||
                                        `Cache upload failed because file read failed with ${error.Message}`
 | 
			
		||||
                                    );
 | 
			
		||||
                                }),
 | 
			
		||||
                        start,
 | 
			
		||||
                        end
 | 
			
		||||
                    );
 | 
			
		||||
@@ -265,9 +362,11 @@ async function commitCache(
 | 
			
		||||
    filesize: number
 | 
			
		||||
): Promise<ITypedResponse<null>> {
 | 
			
		||||
    const commitCacheRequest: CommitCacheRequest = { size: filesize };
 | 
			
		||||
    return await httpClient.postJson<null>(
 | 
			
		||||
        getCacheApiUrl(`caches/${cacheId.toString()}`),
 | 
			
		||||
        commitCacheRequest
 | 
			
		||||
    return await retryTypedResponse("commitCache", () =>
 | 
			
		||||
        httpClient.postJson<null>(
 | 
			
		||||
            getCacheApiUrl(`caches/${cacheId.toString()}`),
 | 
			
		||||
            commitCacheRequest
 | 
			
		||||
        )
 | 
			
		||||
    );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										49
									
								
								src/tar.ts
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								src/tar.ts
									
									
									
									
									
								
							@@ -1,14 +1,36 @@
 | 
			
		||||
import * as core from "@actions/core";
 | 
			
		||||
import { exec } from "@actions/exec";
 | 
			
		||||
import * as io from "@actions/io";
 | 
			
		||||
import { existsSync } from "fs";
 | 
			
		||||
import * as path from "path";
 | 
			
		||||
import * as tar from "./tar";
 | 
			
		||||
 | 
			
		||||
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 tar.isGnuTar()) {
 | 
			
		||||
            args.push("--force-local");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    return await io.which("tar", true);
 | 
			
		||||
@@ -16,14 +38,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 +50,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 +64,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);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user