diff --git a/README.md b/README.md index 0134be9..c80b9da 100644 --- a/README.md +++ b/README.md @@ -454,13 +454,15 @@ jobs: Following inputs can be used as `step.with` keys -| Name | Type | Default | Description | -|------------------|---------|-----------------------------|------------------------------------| -| `registry` | String | | Server address of Docker registry. If not set then will default to Docker Hub | -| `username` | String | | Username used to log against the Docker registry | -| `password` | String | | Password or personal access token used to log against the Docker registry | -| `ecr` | String | `auto` | Specifies whether the given registry is ECR (`auto`, `true` or `false`) | -| `logout` | Bool | `true` | Log out from the Docker registry at the end of a job | +| Name | Type | Default | Description | +|------------ --------|--------|---------|------------------------------------| +| `registry` | String | | Server address of Docker registry. If not set then will default to Docker Hub | +| `username` | String | | Username used to log against the Docker registry | +| `password` | String | | Password or personal access token used to log against the Docker registry | +| `ecr` | String | `auto` | Specifies whether the given registry is ECR (`auto`, `true` or `false`) | +| `logout` | Bool | `true` | Log out from the Docker registry at the end of a job | +| `retries` | String | `3` | Maximum retries in case of errors (limit of 50 hardcoded) | +| `retryErrorPattern` | String | | Regexp to match error message | ## Keep up-to-date with GitHub Dependabot diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 2fcbe87..a8451ab 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -70,3 +70,23 @@ test('calls docker login', async () => { expect(setLogoutSpy).toHaveBeenCalledWith(logout); expect(dockerSpy).toHaveBeenCalledWith(registry, username, password, ecr); }); + +test('retried error without username and password', async () => { + const platSpy = jest.spyOn(osm, 'platform').mockImplementation(() => 'linux'); + + const setFailedSpy = jest.spyOn(core, 'setFailed'); + const warningSpy = jest.spyOn(core, 'warning'); + const dockerSpy = jest.spyOn(docker, 'login'); + dockerSpy.mockImplementation(() => { + throw Error('Username and password required'); + }); + + process.env['INPUT_LOGOUT'] = 'true'; // default value + process.env['INPUT_RETRYERRORPATTERN'] = '.*and password.*'; + process.env['INPUT_RETRIES'] = '5'; + + await run(); + expect(warningSpy).toHaveBeenCalledWith('Error <<>> is recoverable, retrying...'); + expect(warningSpy).toBeCalledTimes(4); + expect(setFailedSpy).toHaveBeenCalledWith('Username and password required'); +}); diff --git a/action.yml b/action.yml index 5e837ac..439c646 100644 --- a/action.yml +++ b/action.yml @@ -24,6 +24,13 @@ inputs: description: 'Log out from the Docker registry at the end of a job' default: 'true' required: false + retryErrorPattern: + description: "Regexp to match error message" + required: false + retries: + description: "Maximum retries in case of errors (limit of 50 hardcoded)" + default: "3" + required: false runs: using: 'node12' diff --git a/dist/index.js b/dist/index.js index 5c056b2..3bbf562 100644 --- a/dist/index.js +++ b/dist/index.js @@ -185,7 +185,9 @@ function getInputs() { username: core.getInput('username'), password: core.getInput('password'), ecr: core.getInput('ecr'), - logout: core.getBooleanInput('logout') + logout: core.getBooleanInput('logout'), + retryErrorPattern: core.getInput('retryErrorPattern'), + retries: core.getInput('retries') }; } exports.getInputs = getInputs; @@ -329,7 +331,23 @@ async function run() { const input = context.getInputs(); stateHelper.setRegistry(input.registry); stateHelper.setLogout(input.logout); - await docker.login(input.registry, input.username, input.password, input.ecr); + let retryErrorPattern = input.retryErrorPattern; + let attemptCount = parseInt(input.retries); + if (isNaN(attemptCount)) + attemptCount = 3; + attemptCount = Math.min(attemptCount, 50); + // eslint-disable-next-line no-constant-condition + while (true) { + try { + await docker.login(input.registry, input.username, input.password, input.ecr); + break; + } + catch (error) { + if (!retryErrorPattern || !RegExp(retryErrorPattern).test(error.message) || --attemptCount <= 0) + throw error; + core.warning(`Error <<<${error.message}>>> is recoverable, retrying...`); + } + } } catch (error) { core.setFailed(error.message); diff --git a/src/context.ts b/src/context.ts index 8a38168..24669b4 100644 --- a/src/context.ts +++ b/src/context.ts @@ -6,6 +6,8 @@ export interface Inputs { password: string; ecr: string; logout: boolean; + retryErrorPattern: string; + retries: string; } export function getInputs(): Inputs { @@ -14,6 +16,8 @@ export function getInputs(): Inputs { username: core.getInput('username'), password: core.getInput('password'), ecr: core.getInput('ecr'), - logout: core.getBooleanInput('logout') + logout: core.getBooleanInput('logout'), + retryErrorPattern: core.getInput('retryErrorPattern'), + retries: core.getInput('retries') }; } diff --git a/src/main.ts b/src/main.ts index c51ffe4..e388a46 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,7 +8,20 @@ export async function run(): Promise { const input: context.Inputs = context.getInputs(); stateHelper.setRegistry(input.registry); stateHelper.setLogout(input.logout); - await docker.login(input.registry, input.username, input.password, input.ecr); + let retryErrorPattern = input.retryErrorPattern; + let attemptCount = parseInt(input.retries); + if (isNaN(attemptCount)) attemptCount = 3; + attemptCount = Math.min(attemptCount, 50); + // eslint-disable-next-line no-constant-condition + while (true) { + try { + await docker.login(input.registry, input.username, input.password, input.ecr); + break; + } catch (error: any) { + if (!retryErrorPattern || !RegExp(retryErrorPattern).test(error.message) || --attemptCount <= 0) throw error; + core.warning(`Error <<<${error.message}>>> is recoverable, retrying...`); + } + } } catch (error: any) { core.setFailed(error.message); }