mirror of
				https://kkgithub.com/actions/cache.git
				synced 2025-10-25 23:42:19 +08:00 
			
		
		
		
	Compare commits
	
		
			29 Commits
		
	
	
		
			bishal/deb
			...
			v1.2.1
		
	
	| 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
	