1
0
Fork 0

Merge pull request #1605 from actions/robherley/usage-message

Better error message for artifact usage limits
pull/1607/head
Rob Herley 2023-12-12 09:49:55 -05:00 committed by GitHub
commit eff198be5b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 75 additions and 13 deletions

View File

@ -4,6 +4,7 @@ import {HttpClient} from '@actions/http-client'
import * as config from '../src/internal/shared/config' import * as config from '../src/internal/shared/config'
import {internalArtifactTwirpClient} from '../src/internal/shared/artifact-twirp-client' import {internalArtifactTwirpClient} from '../src/internal/shared/artifact-twirp-client'
import {noopLogs} from './common' import {noopLogs} from './common'
import {NetworkError, UsageError} from '../src/internal/shared/errors'
jest.mock('@actions/http-client') jest.mock('@actions/http-client')
@ -257,9 +258,42 @@ describe('artifact-http-client', () => {
name: 'artifact', name: 'artifact',
version: 4 version: 4
}) })
}).rejects.toThrowError( }).rejects.toThrowError(new NetworkError('ENOTFOUND').message)
'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)
})
it('should properly describe a usage error', async () => {
const mockPost = jest.fn(() => {
const msgFailed = new http.IncomingMessage(new net.Socket())
msgFailed.statusCode = 403
msgFailed.statusMessage = 'Forbidden'
return {
message: msgFailed,
readBody: async () => {
return Promise.resolve(
`{"msg": "insufficient usage to create artifact"}`
)
}
}
})
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(new UsageError().message)
expect(mockHttpClient).toHaveBeenCalledTimes(1) expect(mockHttpClient).toHaveBeenCalledTimes(1)
expect(mockPost).toHaveBeenCalledTimes(1) expect(mockPost).toHaveBeenCalledTimes(1)
}) })

View File

@ -4,7 +4,7 @@ import {info, debug} from '@actions/core'
import {ArtifactServiceClientJSON} from '../../generated' import {ArtifactServiceClientJSON} from '../../generated'
import {getResultsServiceUrl, getRuntimeToken} from './config' import {getResultsServiceUrl, getRuntimeToken} from './config'
import {getUserAgentString} from './user-agent' import {getUserAgentString} from './user-agent'
import {NetworkError} from './errors' import {NetworkError, UsageError} from './errors'
// The twirp http client must implement this interface // The twirp http client must implement this interface
interface Rpc { interface Rpc {
@ -64,7 +64,7 @@ class ArtifactHttpClient implements Rpc {
this.httpClient.post(url, JSON.stringify(data), headers) this.httpClient.post(url, JSON.stringify(data), headers)
) )
return JSON.parse(body) return body
} catch (error) { } catch (error) {
throw new Error(`Failed to ${method}: ${error.message}`) throw new Error(`Failed to ${method}: ${error.message}`)
} }
@ -72,34 +72,49 @@ class ArtifactHttpClient implements Rpc {
async retryableRequest( async retryableRequest(
operation: () => Promise<HttpClientResponse> operation: () => Promise<HttpClientResponse>
): Promise<{response: HttpClientResponse; body: string}> { ): Promise<{response: HttpClientResponse; body: object}> {
let attempt = 0 let attempt = 0
let errorMessage = '' let errorMessage = ''
let rawBody = ''
while (attempt < this.maxAttempts) { while (attempt < this.maxAttempts) {
let isRetryable = false let isRetryable = false
try { try {
const response = await operation() const response = await operation()
const statusCode = response.message.statusCode const statusCode = response.message.statusCode
const body = await response.readBody() rawBody = await response.readBody()
debug(`[Response] - ${response.message.statusCode}`) debug(`[Response] - ${response.message.statusCode}`)
debug(`Headers: ${JSON.stringify(response.message.headers, null, 2)}`) debug(`Headers: ${JSON.stringify(response.message.headers, null, 2)}`)
debug(`Body: ${body}`) const body = JSON.parse(rawBody)
debug(`Body: ${JSON.stringify(body, null, 2)}`)
if (this.isSuccessStatusCode(statusCode)) { if (this.isSuccessStatusCode(statusCode)) {
return {response, body} return {response, body}
} }
isRetryable = this.isRetryableHttpStatusCode(statusCode) isRetryable = this.isRetryableHttpStatusCode(statusCode)
errorMessage = `Failed request: (${statusCode}) ${response.message.statusMessage}` errorMessage = `Failed request: (${statusCode}) ${response.message.statusMessage}`
const responseMessage = JSON.parse(body).msg if (body.msg) {
if (responseMessage) { if (UsageError.isUsageErrorMessage(body.msg)) {
errorMessage = `${errorMessage}: ${responseMessage}` throw new UsageError()
}
errorMessage = `${errorMessage}: ${body.msg}`
} }
} catch (error) { } catch (error) {
isRetryable = true if (error instanceof SyntaxError) {
errorMessage = error.message debug(`Raw Body: ${rawBody}`)
throw error
}
if (error instanceof UsageError) {
throw error
}
if (NetworkError.isNetworkErrorCode(error?.code)) { if (NetworkError.isNetworkErrorCode(error?.code)) {
throw new NetworkError(error?.code) throw new NetworkError(error?.code)
} }
isRetryable = true
errorMessage = error.message
} }
if (!isRetryable) { if (!isRetryable) {

View File

@ -57,3 +57,16 @@ export class NetworkError extends Error {
].includes(code) ].includes(code)
} }
} }
export class UsageError extends Error {
constructor() {
const message = `Artifact storage quota has been hit. Unable to upload any new artifacts. Usage is recalculated every 6-12 hours.\nMore info on storage limits: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions#calculating-minute-and-storage-spending`
super(message)
this.name = 'UsageError'
}
static isUsageErrorMessage = (msg?: string): boolean => {
if (!msg) return false
return msg.includes('insufficient usage')
}
}