mirror of https://github.com/actions/toolkit
prettier
parent
ad9b955fe9
commit
c0684c5add
|
@ -1,183 +1,200 @@
|
||||||
import * as http from "http"
|
import * as http from 'http'
|
||||||
import * as net from "net"
|
import * as net from 'net'
|
||||||
import { HttpClient } from "@actions/http-client"
|
import {HttpClient} from '@actions/http-client'
|
||||||
import * as config from "../src/internal/shared/config"
|
import * as config from '../src/internal/shared/config'
|
||||||
import { createArtifactTwirpClient } from "../src/internal/shared/artifact-twirp-client"
|
import {createArtifactTwirpClient} from '../src/internal/shared/artifact-twirp-client'
|
||||||
import * as core from "@actions/core"
|
import * as core from '@actions/core'
|
||||||
|
|
||||||
jest.mock("@actions/http-client")
|
jest.mock('@actions/http-client')
|
||||||
|
|
||||||
describe("artifact-http-client", () => {
|
describe('artifact-http-client', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
// mock all output so that there is less noise when running tests
|
// mock all output so that there is less noise when running tests
|
||||||
jest.spyOn(console, "log").mockImplementation(() => {})
|
jest.spyOn(console, 'log').mockImplementation(() => {})
|
||||||
jest.spyOn(core, "debug").mockImplementation(() => {})
|
jest.spyOn(core, 'debug').mockImplementation(() => {})
|
||||||
jest.spyOn(core, "info").mockImplementation(() => {})
|
jest.spyOn(core, 'info').mockImplementation(() => {})
|
||||||
jest.spyOn(core, "warning").mockImplementation(() => {})
|
jest.spyOn(core, 'warning').mockImplementation(() => {})
|
||||||
jest.spyOn(config, "getResultsServiceUrl").mockReturnValue("http://localhost:8080")
|
jest
|
||||||
jest.spyOn(config, "getRuntimeToken").mockReturnValue("token")
|
.spyOn(config, 'getResultsServiceUrl')
|
||||||
|
.mockReturnValue('http://localhost:8080')
|
||||||
|
jest.spyOn(config, 'getRuntimeToken').mockReturnValue('token')
|
||||||
})
|
})
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks()
|
jest.clearAllMocks()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should successfully create a client", () => {
|
it('should successfully create a client', () => {
|
||||||
const client = createArtifactTwirpClient("upload")
|
const client = createArtifactTwirpClient('upload')
|
||||||
expect(client).toBeDefined()
|
expect(client).toBeDefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should make a request", async () => {
|
it('should make a request', async () => {
|
||||||
const mockPost = jest.fn(() => {
|
const mockPost = jest.fn(() => {
|
||||||
const msg = new http.IncomingMessage(new net.Socket())
|
const msg = new http.IncomingMessage(new net.Socket())
|
||||||
msg.statusCode = 200
|
msg.statusCode = 200
|
||||||
return {
|
return {
|
||||||
message: msg,
|
message: msg,
|
||||||
readBody: () => {return Promise.resolve(`{"ok": true, "signedUploadUrl": "http://localhost:8080/upload"}`)}
|
readBody: () => {
|
||||||
|
return Promise.resolve(
|
||||||
|
`{"ok": true, "signedUploadUrl": "http://localhost:8080/upload"}`
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const mockHttpClient = (HttpClient as unknown as jest.Mock).mockImplementation(() => {
|
const mockHttpClient = (
|
||||||
|
HttpClient as unknown as jest.Mock
|
||||||
|
).mockImplementation(() => {
|
||||||
return {
|
return {
|
||||||
post: mockPost
|
post: mockPost
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const client = createArtifactTwirpClient("upload")
|
const client = createArtifactTwirpClient('upload')
|
||||||
const artifact = await client.CreateArtifact(
|
const artifact = await client.CreateArtifact({
|
||||||
{
|
workflowRunBackendId: '1234',
|
||||||
workflowRunBackendId: "1234",
|
workflowJobRunBackendId: '5678',
|
||||||
workflowJobRunBackendId: "5678",
|
name: 'artifact',
|
||||||
name: "artifact",
|
version: 4
|
||||||
version: 4
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(mockHttpClient).toHaveBeenCalledTimes(1)
|
expect(mockHttpClient).toHaveBeenCalledTimes(1)
|
||||||
expect(mockPost).toHaveBeenCalledTimes(1)
|
expect(mockPost).toHaveBeenCalledTimes(1)
|
||||||
expect(artifact).toBeDefined()
|
expect(artifact).toBeDefined()
|
||||||
expect(artifact.ok).toBe(true)
|
expect(artifact.ok).toBe(true)
|
||||||
expect(artifact.signedUploadUrl).toBe("http://localhost:8080/upload")
|
expect(artifact.signedUploadUrl).toBe('http://localhost:8080/upload')
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should retry if the request fails", async () => {
|
it('should retry if the request fails', async () => {
|
||||||
const mockPost = jest
|
const mockPost = jest
|
||||||
.fn(() => {
|
.fn(() => {
|
||||||
const msgSucceeded = new http.IncomingMessage(new net.Socket())
|
const msgSucceeded = new http.IncomingMessage(new net.Socket())
|
||||||
msgSucceeded.statusCode = 200
|
msgSucceeded.statusCode = 200
|
||||||
return {
|
return {
|
||||||
message: msgSucceeded,
|
message: msgSucceeded,
|
||||||
readBody: () => {return Promise.resolve(`{"ok": true, "signedUploadUrl": "http://localhost:8080/upload"}`)}
|
readBody: () => {
|
||||||
}
|
return Promise.resolve(
|
||||||
})
|
`{"ok": true, "signedUploadUrl": "http://localhost:8080/upload"}`
|
||||||
.mockImplementationOnce(() => {
|
)
|
||||||
const msgFailed = new http.IncomingMessage(new net.Socket())
|
}
|
||||||
msgFailed.statusCode = 500
|
}
|
||||||
msgFailed.statusMessage = "Internal Server Error"
|
})
|
||||||
return {
|
.mockImplementationOnce(() => {
|
||||||
message: msgFailed,
|
const msgFailed = new http.IncomingMessage(new net.Socket())
|
||||||
readBody: () => {return Promise.resolve(`{"ok": false}`)}
|
msgFailed.statusCode = 500
|
||||||
}
|
msgFailed.statusMessage = 'Internal Server Error'
|
||||||
})
|
return {
|
||||||
const mockHttpClient = (HttpClient as unknown as jest.Mock).mockImplementation(() => {
|
message: msgFailed,
|
||||||
|
readBody: () => {
|
||||||
|
return Promise.resolve(`{"ok": false}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const mockHttpClient = (
|
||||||
|
HttpClient as unknown as jest.Mock
|
||||||
|
).mockImplementation(() => {
|
||||||
return {
|
return {
|
||||||
post: mockPost
|
post: mockPost
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const client = createArtifactTwirpClient(
|
const client = createArtifactTwirpClient(
|
||||||
"upload",
|
'upload',
|
||||||
5, // retry 5 times
|
5, // retry 5 times
|
||||||
1, // wait 1 ms
|
1, // wait 1 ms
|
||||||
1.5 // backoff factor
|
1.5 // backoff factor
|
||||||
)
|
)
|
||||||
const artifact = await client.CreateArtifact(
|
const artifact = await client.CreateArtifact({
|
||||||
{
|
workflowRunBackendId: '1234',
|
||||||
workflowRunBackendId: "1234",
|
workflowJobRunBackendId: '5678',
|
||||||
workflowJobRunBackendId: "5678",
|
name: 'artifact',
|
||||||
name: "artifact",
|
version: 4
|
||||||
version: 4
|
})
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(mockHttpClient).toHaveBeenCalledTimes(1)
|
expect(mockHttpClient).toHaveBeenCalledTimes(1)
|
||||||
expect(artifact).toBeDefined()
|
expect(artifact).toBeDefined()
|
||||||
expect(artifact.ok).toBe(true)
|
expect(artifact.ok).toBe(true)
|
||||||
expect(artifact.signedUploadUrl).toBe("http://localhost:8080/upload")
|
expect(artifact.signedUploadUrl).toBe('http://localhost:8080/upload')
|
||||||
expect(mockPost).toHaveBeenCalledTimes(2)
|
expect(mockPost).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should fail if the request fails 5 times", async () => {
|
it('should fail if the request fails 5 times', async () => {
|
||||||
const mockPost = jest
|
const mockPost = jest.fn(() => {
|
||||||
.fn(() => {
|
const msgFailed = new http.IncomingMessage(new net.Socket())
|
||||||
const msgFailed = new http.IncomingMessage(new net.Socket())
|
msgFailed.statusCode = 500
|
||||||
msgFailed.statusCode = 500
|
msgFailed.statusMessage = 'Internal Server Error'
|
||||||
msgFailed.statusMessage = "Internal Server Error"
|
return {
|
||||||
return {
|
message: msgFailed,
|
||||||
message: msgFailed,
|
readBody: () => {
|
||||||
readBody: () => {return Promise.resolve(`{"ok": false}`)}
|
return Promise.resolve(`{"ok": false}`)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const mockHttpClient = (HttpClient as unknown as jest.Mock).mockImplementation(() => {
|
const mockHttpClient = (
|
||||||
return {
|
HttpClient as unknown as jest.Mock
|
||||||
post: mockPost
|
).mockImplementation(() => {
|
||||||
}
|
return {
|
||||||
})
|
post: mockPost
|
||||||
const client = createArtifactTwirpClient(
|
}
|
||||||
"upload",
|
})
|
||||||
5, // retry 5 times
|
const client = createArtifactTwirpClient(
|
||||||
1, // wait 1 ms
|
'upload',
|
||||||
1.5 // backoff factor
|
5, // retry 5 times
|
||||||
)
|
1, // wait 1 ms
|
||||||
|
1.5 // backoff factor
|
||||||
|
)
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
await client.CreateArtifact(
|
await client.CreateArtifact({
|
||||||
{
|
workflowRunBackendId: '1234',
|
||||||
workflowRunBackendId: "1234",
|
workflowJobRunBackendId: '5678',
|
||||||
workflowJobRunBackendId: "5678",
|
name: 'artifact',
|
||||||
name: "artifact",
|
version: 4
|
||||||
version: 4
|
})
|
||||||
}
|
}).rejects.toThrowError(
|
||||||
)
|
'Failed to make request after 5 attempts: Failed request: (500) Internal Server Error'
|
||||||
}).rejects.toThrowError("Failed to make request after 5 attempts: Failed request: (500) Internal Server Error")
|
)
|
||||||
expect(mockHttpClient).toHaveBeenCalledTimes(1)
|
expect(mockHttpClient).toHaveBeenCalledTimes(1)
|
||||||
expect(mockPost).toHaveBeenCalledTimes(5)
|
expect(mockPost).toHaveBeenCalledTimes(5)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should fail immediately if there is a non-retryable error", async () => {
|
|
||||||
const mockPost = jest
|
|
||||||
.fn(() => {
|
|
||||||
const msgFailed = new http.IncomingMessage(new net.Socket())
|
|
||||||
msgFailed.statusCode = 401
|
|
||||||
msgFailed.statusMessage = "Unauthorized"
|
|
||||||
return {
|
|
||||||
message: msgFailed,
|
|
||||||
readBody: () => {return Promise.resolve(`{"ok": false}`)}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const mockHttpClient = (HttpClient as unknown as jest.Mock).mockImplementation(() => {
|
it('should fail immediately if there is a non-retryable error', async () => {
|
||||||
return {
|
const mockPost = jest.fn(() => {
|
||||||
post: mockPost
|
const msgFailed = new http.IncomingMessage(new net.Socket())
|
||||||
|
msgFailed.statusCode = 401
|
||||||
|
msgFailed.statusMessage = 'Unauthorized'
|
||||||
|
return {
|
||||||
|
message: msgFailed,
|
||||||
|
readBody: () => {
|
||||||
|
return Promise.resolve(`{"ok": false}`)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
const client = createArtifactTwirpClient(
|
})
|
||||||
"upload",
|
|
||||||
5, // retry 5 times
|
const mockHttpClient = (
|
||||||
1, // wait 1 ms
|
HttpClient as unknown as jest.Mock
|
||||||
1.5 // backoff factor
|
).mockImplementation(() => {
|
||||||
)
|
return {
|
||||||
|
post: mockPost
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const client = createArtifactTwirpClient(
|
||||||
|
'upload',
|
||||||
|
5, // retry 5 times
|
||||||
|
1, // wait 1 ms
|
||||||
|
1.5 // backoff factor
|
||||||
|
)
|
||||||
await expect(async () => {
|
await expect(async () => {
|
||||||
await client.CreateArtifact(
|
await client.CreateArtifact({
|
||||||
{
|
workflowRunBackendId: '1234',
|
||||||
workflowRunBackendId: "1234",
|
workflowJobRunBackendId: '5678',
|
||||||
workflowJobRunBackendId: "5678",
|
name: 'artifact',
|
||||||
name: "artifact",
|
version: 4
|
||||||
version: 4
|
})
|
||||||
}
|
}).rejects.toThrowError(
|
||||||
)
|
'Received non-retryable error: Failed request: (401) Unauthorized'
|
||||||
}).rejects.toThrowError("Received non-retryable error: Failed request: (401) Unauthorized")
|
)
|
||||||
expect(mockHttpClient).toHaveBeenCalledTimes(1)
|
expect(mockHttpClient).toHaveBeenCalledTimes(1)
|
||||||
expect(mockPost).toHaveBeenCalledTimes(1)
|
expect(mockPost).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
import { HttpClient, HttpClientResponse, HttpCodes } from "@actions/http-client"
|
import {HttpClient, HttpClientResponse, HttpCodes} from '@actions/http-client'
|
||||||
import { BearerCredentialHandler } from "@actions/http-client/lib/auth"
|
import {BearerCredentialHandler} from '@actions/http-client/lib/auth'
|
||||||
import { info } from "@actions/core"
|
import {info} from '@actions/core'
|
||||||
import { ArtifactServiceClientJSON } from "../../generated"
|
import {ArtifactServiceClientJSON} from '../../generated'
|
||||||
import { getResultsServiceUrl, getRuntimeToken } from "./config"
|
import {getResultsServiceUrl, getRuntimeToken} from './config'
|
||||||
|
|
||||||
// The twirp http client must implement this interface
|
// The twirp http client must implement this interface
|
||||||
interface Rpc {
|
interface Rpc {
|
||||||
request(
|
request(
|
||||||
service: string,
|
service: string,
|
||||||
method: string,
|
method: string,
|
||||||
contentType: "application/json" | "application/protobuf",
|
contentType: 'application/json' | 'application/protobuf',
|
||||||
data: object | Uint8Array
|
data: object | Uint8Array
|
||||||
): Promise<object | Uint8Array>
|
): Promise<object | Uint8Array>
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,12 @@ class ArtifactHttpClient implements Rpc {
|
||||||
private baseRetryIntervalMilliseconds: number = 3000
|
private baseRetryIntervalMilliseconds: number = 3000
|
||||||
private retryMultiplier: number = 1.5
|
private retryMultiplier: number = 1.5
|
||||||
|
|
||||||
constructor(userAgent: string, maxAttempts?: number, baseRetryIntervalMilliseconds?: number, retryMultiplier?: number) {
|
constructor(
|
||||||
|
userAgent: string,
|
||||||
|
maxAttempts?: number,
|
||||||
|
baseRetryIntervalMilliseconds?: number,
|
||||||
|
retryMultiplier?: number
|
||||||
|
) {
|
||||||
const token = getRuntimeToken()
|
const token = getRuntimeToken()
|
||||||
this.baseUrl = getResultsServiceUrl()
|
this.baseUrl = getResultsServiceUrl()
|
||||||
if (maxAttempts) {
|
if (maxAttempts) {
|
||||||
|
@ -34,10 +39,9 @@ class ArtifactHttpClient implements Rpc {
|
||||||
this.retryMultiplier = retryMultiplier
|
this.retryMultiplier = retryMultiplier
|
||||||
}
|
}
|
||||||
|
|
||||||
this.httpClient = new HttpClient(
|
this.httpClient = new HttpClient(userAgent, [
|
||||||
userAgent,
|
new BearerCredentialHandler(token)
|
||||||
[new BearerCredentialHandler(token)],
|
])
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function satisfies the Rpc interface. It is compatible with the JSON
|
// This function satisfies the Rpc interface. It is compatible with the JSON
|
||||||
|
@ -45,17 +49,19 @@ class ArtifactHttpClient implements Rpc {
|
||||||
async request(
|
async request(
|
||||||
service: string,
|
service: string,
|
||||||
method: string,
|
method: string,
|
||||||
contentType: "application/json" | "application/protobuf",
|
contentType: 'application/json' | 'application/protobuf',
|
||||||
data: object | Uint8Array
|
data: object | Uint8Array
|
||||||
): Promise<object | Uint8Array> {
|
): Promise<object | Uint8Array> {
|
||||||
let url = `${this.baseUrl}/twirp/${service}/${method}`
|
let url = `${this.baseUrl}/twirp/${service}/${method}`
|
||||||
let headers = {
|
let headers = {
|
||||||
"Content-Type": contentType,
|
'Content-Type': contentType
|
||||||
}
|
}
|
||||||
info(`Making request to ${url} with data: ${JSON.stringify(data)}`)
|
info(`Making request to ${url} with data: ${JSON.stringify(data)}`)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.retryableRequest(() => this.httpClient.post(url, JSON.stringify(data), headers))
|
const response = await this.retryableRequest(() =>
|
||||||
|
this.httpClient.post(url, JSON.stringify(data), headers)
|
||||||
|
)
|
||||||
const body = await response.readBody()
|
const body = await response.readBody()
|
||||||
return JSON.parse(body)
|
return JSON.parse(body)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -64,10 +70,10 @@ class ArtifactHttpClient implements Rpc {
|
||||||
}
|
}
|
||||||
|
|
||||||
async retryableRequest(
|
async retryableRequest(
|
||||||
operation: () => Promise<HttpClientResponse>,
|
operation: () => Promise<HttpClientResponse>
|
||||||
): Promise<HttpClientResponse> {
|
): Promise<HttpClientResponse> {
|
||||||
let attempt = 0
|
let attempt = 0
|
||||||
let errorMessage = ""
|
let errorMessage = ''
|
||||||
while (attempt < this.maxAttempts) {
|
while (attempt < this.maxAttempts) {
|
||||||
let isRetryable = false
|
let isRetryable = false
|
||||||
|
|
||||||
|
@ -91,12 +97,17 @@ class ArtifactHttpClient implements Rpc {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attempt + 1 === this.maxAttempts) {
|
if (attempt + 1 === this.maxAttempts) {
|
||||||
throw new Error(`Failed to make request after ${this.maxAttempts} attempts: ${errorMessage}`)
|
throw new Error(
|
||||||
|
`Failed to make request after ${this.maxAttempts} attempts: ${errorMessage}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const retryTimeMilliseconds = this.getExponentialRetryTimeMilliseconds(attempt)
|
const retryTimeMilliseconds =
|
||||||
|
this.getExponentialRetryTimeMilliseconds(attempt)
|
||||||
info(
|
info(
|
||||||
`Attempt ${attempt + 1} of ${this.maxAttempts} failed with error: ${errorMessage}. Retrying request in ${retryTimeMilliseconds} ms...`
|
`Attempt ${attempt + 1} of ${
|
||||||
|
this.maxAttempts
|
||||||
|
} failed with error: ${errorMessage}. Retrying request in ${retryTimeMilliseconds} ms...`
|
||||||
)
|
)
|
||||||
await this.sleep(retryTimeMilliseconds)
|
await this.sleep(retryTimeMilliseconds)
|
||||||
attempt++
|
attempt++
|
||||||
|
@ -131,14 +142,15 @@ class ArtifactHttpClient implements Rpc {
|
||||||
|
|
||||||
getExponentialRetryTimeMilliseconds(attempt: number): number {
|
getExponentialRetryTimeMilliseconds(attempt: number): number {
|
||||||
if (attempt < 0) {
|
if (attempt < 0) {
|
||||||
throw new Error("attempt should be a positive integer")
|
throw new Error('attempt should be a positive integer')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attempt === 0) {
|
if (attempt === 0) {
|
||||||
return this.baseRetryIntervalMilliseconds
|
return this.baseRetryIntervalMilliseconds
|
||||||
}
|
}
|
||||||
|
|
||||||
const minTime = this.baseRetryIntervalMilliseconds * this.retryMultiplier ** (attempt)
|
const minTime =
|
||||||
|
this.baseRetryIntervalMilliseconds * this.retryMultiplier ** attempt
|
||||||
const maxTime = minTime * this.retryMultiplier
|
const maxTime = minTime * this.retryMultiplier
|
||||||
|
|
||||||
// returns a random number between minTime and maxTime (exclusive)
|
// returns a random number between minTime and maxTime (exclusive)
|
||||||
|
@ -147,15 +159,15 @@ class ArtifactHttpClient implements Rpc {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createArtifactTwirpClient(
|
export function createArtifactTwirpClient(
|
||||||
type: "upload" | "download",
|
type: 'upload' | 'download',
|
||||||
maxAttempts?: number,
|
maxAttempts?: number,
|
||||||
baseRetryIntervalMilliseconds?: number,
|
baseRetryIntervalMilliseconds?: number,
|
||||||
retryMultiplier?: number
|
retryMultiplier?: number
|
||||||
): ArtifactServiceClientJSON {
|
): ArtifactServiceClientJSON {
|
||||||
const client = new ArtifactHttpClient(
|
const client = new ArtifactHttpClient(
|
||||||
`@actions/artifact-${type}`,
|
`@actions/artifact-${type}`,
|
||||||
maxAttempts,
|
maxAttempts,
|
||||||
baseRetryIntervalMilliseconds,
|
baseRetryIntervalMilliseconds,
|
||||||
retryMultiplier
|
retryMultiplier
|
||||||
)
|
)
|
||||||
return new ArtifactServiceClientJSON(client)
|
return new ArtifactServiceClientJSON(client)
|
||||||
|
|
Loading…
Reference in New Issue