diff --git a/packages/artifact/__tests__/artifact-http-client.test.ts b/packages/artifact/__tests__/artifact-http-client.test.ts index 3c6c3784..d96c0d59 100644 --- a/packages/artifact/__tests__/artifact-http-client.test.ts +++ b/packages/artifact/__tests__/artifact-http-client.test.ts @@ -7,6 +7,12 @@ import {noopLogs} from './common' jest.mock('@actions/http-client') +const clientOptions = { + maxAttempts: 5, + retryIntervalMs: 1, + retryMultiplier: 1.5 +} + describe('artifact-http-client', () => { beforeAll(() => { noopLogs() @@ -94,11 +100,7 @@ describe('artifact-http-client', () => { } }) - const client = internalArtifactTwirpClient({ - maxAttempts: 5, - retryIntervalMs: 1, - retryMultiplier: 1.5 - }) + const client = internalArtifactTwirpClient(clientOptions) const artifact = await client.CreateArtifact({ workflowRunBackendId: '1234', workflowJobRunBackendId: '5678', @@ -133,11 +135,7 @@ describe('artifact-http-client', () => { post: mockPost } }) - const client = internalArtifactTwirpClient({ - maxAttempts: 5, - retryIntervalMs: 1, - retryMultiplier: 1.5 - }) + const client = internalArtifactTwirpClient(clientOptions) await expect(async () => { await client.CreateArtifact({ workflowRunBackendId: '1234', @@ -172,11 +170,7 @@ describe('artifact-http-client', () => { post: mockPost } }) - const client = internalArtifactTwirpClient({ - maxAttempts: 5, - retryIntervalMs: 1, - retryMultiplier: 1.5 - }) + const client = internalArtifactTwirpClient(clientOptions) await expect(async () => { await client.CreateArtifact({ workflowRunBackendId: '1234', @@ -214,11 +208,7 @@ describe('artifact-http-client', () => { post: mockPost } }) - const client = internalArtifactTwirpClient({ - maxAttempts: 5, - retryIntervalMs: 1, - retryMultiplier: 1.5 - }) + const client = internalArtifactTwirpClient(clientOptions) await expect(async () => { await client.CreateArtifact({ workflowRunBackendId: '1234', @@ -238,4 +228,39 @@ describe('artifact-http-client', () => { expect(mockHttpClient).toHaveBeenCalledTimes(1) expect(mockPost).toHaveBeenCalledTimes(1) }) + + it('should properly describe a network failure', async () => { + class FakeNodeError extends Error { + code: string + constructor(code: string) { + super() + this.code = code + } + } + + const mockPost = jest.fn(() => { + throw new FakeNodeError('ENOTFOUND') + }) + + const mockHttpClient = ( + HttpClient as unknown as jest.Mock + ).mockImplementation(() => { + return { + post: mockPost + } + }) + const client = internalArtifactTwirpClient() + await expect(async () => { + await client.CreateArtifact({ + workflowRunBackendId: '1234', + workflowJobRunBackendId: '5678', + name: 'artifact', + version: 4 + }) + }).rejects.toThrowError( + 'Failed to CreateArtifact: Unable to make request: ENOTFOUND\nIf you are using self-hosted runners, please make sure your runner has access to all GitHub endpoints: https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#communication-between-self-hosted-runners-and-github' + ) + expect(mockHttpClient).toHaveBeenCalledTimes(1) + expect(mockPost).toHaveBeenCalledTimes(1) + }) }) diff --git a/packages/artifact/src/internal/shared/artifact-twirp-client.ts b/packages/artifact/src/internal/shared/artifact-twirp-client.ts index 4feba626..be26c648 100644 --- a/packages/artifact/src/internal/shared/artifact-twirp-client.ts +++ b/packages/artifact/src/internal/shared/artifact-twirp-client.ts @@ -4,6 +4,7 @@ import {info, debug} from '@actions/core' import {ArtifactServiceClientJSON} from '../../generated' import {getResultsServiceUrl, getRuntimeToken} from './config' import {getUserAgentString} from './user-agent' +import {NetworkError} from './errors' // The twirp http client must implement this interface interface Rpc { @@ -96,6 +97,9 @@ class ArtifactHttpClient implements Rpc { } catch (error) { isRetryable = true errorMessage = error.message + if (NetworkError.isNetworkErrorCode(error?.code)) { + throw new NetworkError(error?.code) + } } if (!isRetryable) { diff --git a/packages/artifact/src/internal/shared/errors.ts b/packages/artifact/src/internal/shared/errors.ts index 841c44a6..4f23ecd3 100644 --- a/packages/artifact/src/internal/shared/errors.ts +++ b/packages/artifact/src/internal/shared/errors.ts @@ -35,3 +35,25 @@ export class GHESNotSupportedError extends Error { this.name = 'GHESNotSupportedError' } } + +export class NetworkError extends Error { + code: string + + constructor(code: string) { + const message = `Unable to make request: ${code}\nIf you are using self-hosted runners, please make sure your runner has access to all GitHub endpoints: https://docs.github.com/en/actions/hosting-your-own-runners/managing-self-hosted-runners/about-self-hosted-runners#communication-between-self-hosted-runners-and-github` + super(message) + this.code = code + this.name = 'NetworkError' + } + + static isNetworkErrorCode = (code?: string): boolean => { + if (!code) return false + return [ + 'ECONNRESET', + 'ENOTFOUND', + 'ETIMEDOUT', + 'ECONNREFUSED', + 'EHOSTUNREACH' + ].includes(code) + } +} diff --git a/packages/artifact/src/internal/upload/blob-upload.ts b/packages/artifact/src/internal/upload/blob-upload.ts index 59472385..87bb7237 100644 --- a/packages/artifact/src/internal/upload/blob-upload.ts +++ b/packages/artifact/src/internal/upload/blob-upload.ts @@ -5,6 +5,7 @@ import {getUploadChunkSize, getConcurrency} from '../shared/config' import * as core from '@actions/core' import * as crypto from 'crypto' import * as stream from 'stream' +import {NetworkError} from '../shared/errors' export interface BlobUploadResponse { /** @@ -52,12 +53,20 @@ export async function uploadZipToBlobStorage( core.info('Beginning upload of artifact content to blob storage') - await blockBlobClient.uploadStream( - uploadStream, - bufferSize, - maxConcurrency, - options - ) + try { + await blockBlobClient.uploadStream( + uploadStream, + bufferSize, + maxConcurrency, + options + ) + } catch (error) { + if (NetworkError.isNetworkErrorCode(error?.code)) { + throw new NetworkError(error?.code) + } + + throw error + } core.info('Finished uploading artifact content to blob storage!')