This commit is contained in:
Bob Vincent 2026-06-26 16:40:44 -04:00 committed by GitHub
commit f6f8c1b187
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 72 additions and 18 deletions

View File

@ -23,11 +23,9 @@ jest.unstable_mockModule('@actions/core', () => ({
// Dynamic imports after mocking // Dynamic imports after mocking
const {RetryHelper} = await import('../src/retry-helper.js') const {RetryHelper} = await import('../src/retry-helper.js')
let retryHelper: any
describe('retry-helper tests', () => { describe('retry-helper tests', () => {
beforeAll(() => { beforeAll(() => {
retryHelper = new RetryHelper(3, 0, 0) // @actions/core is mocked at module load above; nothing to set up here.
}) })
beforeEach(() => { beforeEach(() => {
@ -40,14 +38,22 @@ describe('retry-helper tests', () => {
}) })
it('first attempt succeeds', async () => { it('first attempt succeeds', async () => {
const retryHelper: any = new RetryHelper(3, 1, 10)
const sleep = jest.fn().mockResolvedValue(undefined)
retryHelper.sleep = sleep
const actual = await retryHelper.execute(async () => { const actual = await retryHelper.execute(async () => {
return 'some result' return 'some result'
}) })
expect(actual).toBe('some result') expect(actual).toBe('some result')
expect(info).toHaveLength(0) expect(info).toHaveLength(0)
expect(sleep).not.toHaveBeenCalled()
}) })
it('second attempt succeeds', async () => { it('second attempt succeeds', async () => {
const retryHelper: any = new RetryHelper(3, 1, 10)
const sleep = jest.fn().mockResolvedValue(undefined)
retryHelper.sleep = sleep
let attempts = 0 let attempts = 0
const actual = await retryHelper.execute(() => { const actual = await retryHelper.execute(() => {
if (++attempts == 1) { if (++attempts == 1) {
@ -60,10 +66,15 @@ describe('retry-helper tests', () => {
expect(actual).toBe('some result') expect(actual).toBe('some result')
expect(info).toHaveLength(2) expect(info).toHaveLength(2)
expect(info[0]).toBe('some error') expect(info[0]).toBe('some error')
expect(info[1]).toMatch(/Waiting .+ seconds before trying again/) expect(info[1]).toBe('Waiting 1 seconds before trying again')
expect(sleep).toHaveBeenCalledTimes(1)
expect(sleep).toHaveBeenCalledWith(1)
}) })
it('third attempt succeeds', async () => { it('third attempt succeeds', async () => {
const retryHelper: any = new RetryHelper(3, 1, 10)
const sleep = jest.fn().mockResolvedValue(undefined)
retryHelper.sleep = sleep
let attempts = 0 let attempts = 0
const actual = await retryHelper.execute(() => { const actual = await retryHelper.execute(() => {
if (++attempts < 3) { if (++attempts < 3) {
@ -76,12 +87,18 @@ describe('retry-helper tests', () => {
expect(actual).toBe('some result') expect(actual).toBe('some result')
expect(info).toHaveLength(4) expect(info).toHaveLength(4)
expect(info[0]).toBe('some error 1') expect(info[0]).toBe('some error 1')
expect(info[1]).toMatch(/Waiting .+ seconds before trying again/) expect(info[1]).toBe('Waiting 1 seconds before trying again')
expect(info[2]).toBe('some error 2') expect(info[2]).toBe('some error 2')
expect(info[3]).toMatch(/Waiting .+ seconds before trying again/) expect(info[3]).toBe('Waiting 2 seconds before trying again')
expect(sleep).toHaveBeenCalledTimes(2)
expect(sleep).toHaveBeenNthCalledWith(1, 1)
expect(sleep).toHaveBeenNthCalledWith(2, 2)
}) })
it('all attempts fail succeeds', async () => { it('all attempts fail succeeds', async () => {
const retryHelper: any = new RetryHelper(3, 1, 10)
const sleep = jest.fn().mockResolvedValue(undefined)
retryHelper.sleep = sleep
let attempts = 0 let attempts = 0
let error: Error = null as unknown as Error let error: Error = null as unknown as Error
try { try {
@ -95,8 +112,42 @@ describe('retry-helper tests', () => {
expect(attempts).toBe(3) expect(attempts).toBe(3)
expect(info).toHaveLength(4) expect(info).toHaveLength(4)
expect(info[0]).toBe('some error 1') expect(info[0]).toBe('some error 1')
expect(info[1]).toMatch(/Waiting .+ seconds before trying again/) expect(info[1]).toBe('Waiting 1 seconds before trying again')
expect(info[2]).toBe('some error 2') expect(info[2]).toBe('some error 2')
expect(info[3]).toMatch(/Waiting .+ seconds before trying again/) expect(info[3]).toBe('Waiting 2 seconds before trying again')
expect(sleep).toHaveBeenCalledTimes(2)
expect(sleep).toHaveBeenNthCalledWith(1, 1)
expect(sleep).toHaveBeenNthCalledWith(2, 2)
})
it('server-side 500 errors are retried with exponential backoff', async () => {
const retryHelper: any = new RetryHelper(4, 2, 10)
const sleep = jest.fn().mockResolvedValue(undefined)
retryHelper.sleep = sleep
let attempts = 0
const actual = await retryHelper.execute(() => {
if (++attempts < 3) {
const error: Error & {status?: number} = new Error(
`server error ${attempts}`
)
error.status = 500
throw error
}
return Promise.resolve('some result')
})
expect(actual).toBe('some result')
expect(attempts).toBe(3)
expect(info).toEqual([
'server error 1',
'Waiting 2 seconds before trying again',
'server error 2',
'Waiting 4 seconds before trying again'
])
expect(sleep).toHaveBeenCalledTimes(2)
expect(sleep).toHaveBeenNthCalledWith(1, 2)
expect(sleep).toHaveBeenNthCalledWith(2, 4)
}) })
}) })

10
dist/index.js vendored
View File

@ -35491,7 +35491,7 @@ class retry_helper_RetryHelper {
info(err?.message); info(err?.message);
} }
// Sleep // Sleep
const seconds = this.getSleepAmount(); const seconds = this.getSleepAmount(attempt);
info(`Waiting ${seconds} seconds before trying again`); info(`Waiting ${seconds} seconds before trying again`);
await this.sleep(seconds); await this.sleep(seconds);
attempt++; attempt++;
@ -35499,9 +35499,11 @@ class retry_helper_RetryHelper {
// Last attempt // Last attempt
return await action(); return await action();
} }
getSleepAmount() { getSleepAmount(attempt) {
return (Math.floor(Math.random() * (this.maxSeconds - this.minSeconds + 1)) + if (this.minSeconds === 0) {
this.minSeconds); return 0;
}
return Math.min(this.minSeconds * Math.pow(2, attempt - 1), this.maxSeconds);
} }
async sleep(seconds) { async sleep(seconds) {
return new Promise(resolve => setTimeout(resolve, seconds * 1000)); return new Promise(resolve => setTimeout(resolve, seconds * 1000));

View File

@ -33,7 +33,7 @@ export class RetryHelper {
} }
// Sleep // Sleep
const seconds = this.getSleepAmount() const seconds = this.getSleepAmount(attempt)
core.info(`Waiting ${seconds} seconds before trying again`) core.info(`Waiting ${seconds} seconds before trying again`)
await this.sleep(seconds) await this.sleep(seconds)
attempt++ attempt++
@ -43,11 +43,12 @@ export class RetryHelper {
return await action() return await action()
} }
private getSleepAmount(): number { private getSleepAmount(attempt: number): number {
return ( if (this.minSeconds === 0) {
Math.floor(Math.random() * (this.maxSeconds - this.minSeconds + 1)) + return 0
this.minSeconds }
)
return Math.min(this.minSeconds * Math.pow(2, attempt - 1), this.maxSeconds)
} }
private async sleep(seconds: number): Promise<void> { private async sleep(seconds: number): Promise<void> {