From 7c68161adf97a48beb850a595b8784ec57a98cbb Mon Sep 17 00:00:00 2001 From: Nick Fields <50085412+nick-invision@users.noreply.github.com> Date: Mon, 4 Jan 2021 21:32:32 -0500 Subject: [PATCH] Add on_retry_command input to optionally run cmd before a retry (#33) * minor: add on_retry_command input to optionally run cmd before a retry * test: add test for on-retry-command failure --- .github/workflows/ci_cd.yml | 20 ++++++++++ .releaserc.js | 1 + README.md | 15 ++++++++ action.yml | 3 ++ dist/index.js | 76 +++++++++++++++++++++++++------------ src/index.ts | 17 ++++++++- 6 files changed, 107 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index b111d88..4cdbe7b 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -60,6 +60,26 @@ jobs: actual: ${{ steps.sad_path_wait_sec.outputs.exit_error }} comparison: contains + - name: on-retry-cmd + id: on-retry-cmd + uses: ./ + continue-on-error: true + with: + timeout_minutes: 1 + max_attempts: 3 + command: node -e "process.exit(1)" + on_retry_command: node -e "console.log('this is a retry command')" + + - name: on-retry-cmd (on-retry fails) + id: on-retry-cmd-fails + uses: ./ + continue-on-error: true + with: + timeout_minutes: 1 + max_attempts: 3 + command: node -e "process.exit(1)" + on_retry_command: node -e "throw new Error('This is an on-retry command error')" + - name: sad-path (error) id: sad_path_error uses: ./ diff --git a/.releaserc.js b/.releaserc.js index f7b6c6c..d42edea 100644 --- a/.releaserc.js +++ b/.releaserc.js @@ -8,6 +8,7 @@ module.exports = { { type: 'minor', release: 'minor' }, { type: 'major', release: 'major' }, { type: 'patch', release: 'patch' }, + { type: 'test', release: false }, { scope: 'no-release', release: false }, ], }, diff --git a/README.md b/README.md index 1994f8a..6753de7 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,10 @@ Retries an Action step on failure or timeout. This is currently intended to repl **Optional** Whether to output a warning on retry, or just output to info. Defaults to `true`. +### `on_retry_command` + +**Optional** Command to run before a retry (such as a cleanup script). Any error thrown from retry command is caught and surfaced as a warning. + ## Outputs ### `total_attempts` @@ -138,6 +142,17 @@ with: actual: ${{ steps.retry.outputs.total_attempts }} ``` +### Run script after failure but before retry + +```yaml +uses: nick-invision/retry@v2 +with: + timeout_seconds: 15 + max_attempts: 3 + command: npm run some-flaky-script-that-outputs-something + on_retry_command: npm run cleanup-flaky-script-output +``` + ## Requirements NodeJS is required for this action to run. This runs without issue on all GitHub hosted runners but if you are running into issues with this on self hosted runners ensure NodeJS is installed. diff --git a/action.yml b/action.yml index 622c855..cc82eea 100644 --- a/action.yml +++ b/action.yml @@ -30,6 +30,9 @@ inputs: warning_on_retry: description: Whether to output a warning on retry, or just output to info. Defaults to true default: true + on_retry_command: + description: Command to run before a retry (such as a cleanup script). Any error thrown from retry command is caught and surfaced as a warning. + required: false outputs: total_attempts: description: The final number of attempts made diff --git a/dist/index.js b/dist/index.js index d21117f..cc4d08c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -253,6 +253,7 @@ var SHELL = core_1.getInput('shell'); var POLLING_INTERVAL_SECONDS = getInputNumber('polling_interval_seconds', false) || 1; var RETRY_ON = core_1.getInput('retry_on') || 'any'; var WARNING_ON_RETRY = core_1.getInput('warning_on_retry').toLowerCase() === 'true'; +var ON_RETRY_COMMAND = core_1.getInput('on_retry_command'); var OS = process.platform; var OUTPUT_TOTAL_ATTEMPTS_KEY = 'total_attempts'; var OUTPUT_EXIT_CODE_KEY = 'exit_code'; @@ -340,6 +341,32 @@ function getExecutable() { } return executable; } +function runRetryCmd() { + return __awaiter(this, void 0, void 0, function () { + var error_1; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: + // if no retry script, just continue + if (!ON_RETRY_COMMAND) { + return [2 /*return*/]; + } + _a.label = 1; + case 1: + _a.trys.push([1, 3, , 4]); + return [4 /*yield*/, child_process_1.execSync(ON_RETRY_COMMAND, { stdio: 'inherit' })]; + case 2: + _a.sent(); + return [3 /*break*/, 4]; + case 3: + error_1 = _a.sent(); + core_1.info("WARNING: Retry command threw the error " + error_1.message); + return [3 /*break*/, 4]; + case 4: return [2 /*return*/]; + } + }); + }); +} function runCmd() { var _a, _b; return __awaiter(this, void 0, void 0, function () { @@ -399,7 +426,7 @@ function runCmd() { } function runAction() { return __awaiter(this, void 0, void 0, function () { - var attempt, error_1; + var attempt, error_2; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, validateInputs()]; @@ -408,43 +435,44 @@ function runAction() { attempt = 1; _a.label = 2; case 2: - if (!(attempt <= MAX_ATTEMPTS)) return [3 /*break*/, 7]; + if (!(attempt <= MAX_ATTEMPTS)) return [3 /*break*/, 12]; _a.label = 3; case 3: - _a.trys.push([3, 5, , 6]); + _a.trys.push([3, 5, , 11]); // just keep overwriting attempts output core_1.setOutput(OUTPUT_TOTAL_ATTEMPTS_KEY, attempt); return [4 /*yield*/, runCmd()]; case 4: _a.sent(); core_1.info("Command completed after " + attempt + " attempt(s)."); - return [3 /*break*/, 7]; + return [3 /*break*/, 12]; case 5: - error_1 = _a.sent(); - if (attempt === MAX_ATTEMPTS) { - throw new Error("Final attempt failed. " + error_1.message); - } - else if (!done && RETRY_ON === 'error') { - // error: timeout - throw error_1; - } - else if (exit > 0 && RETRY_ON === 'timeout') { - // error: error - throw error_1; + error_2 = _a.sent(); + if (!(attempt === MAX_ATTEMPTS)) return [3 /*break*/, 6]; + throw new Error("Final attempt failed. " + error_2.message); + case 6: + if (!(!done && RETRY_ON === 'error')) return [3 /*break*/, 7]; + // error: timeout + throw error_2; + case 7: + if (!(exit > 0 && RETRY_ON === 'timeout')) return [3 /*break*/, 8]; + // error: error + throw error_2; + case 8: return [4 /*yield*/, runRetryCmd()]; + case 9: + _a.sent(); + if (WARNING_ON_RETRY) { + core_1.warning("Attempt " + attempt + " failed. Reason: " + error_2.message); } else { - if (WARNING_ON_RETRY) { - core_1.warning("Attempt " + attempt + " failed. Reason: " + error_1.message); - } - else { - core_1.info("Attempt " + attempt + " failed. Reason: " + error_1.message); - } + core_1.info("Attempt " + attempt + " failed. Reason: " + error_2.message); } - return [3 /*break*/, 6]; - case 6: + _a.label = 10; + case 10: return [3 /*break*/, 11]; + case 11: attempt++; return [3 /*break*/, 2]; - case 7: return [2 /*return*/]; + case 12: return [2 /*return*/]; } }); }); diff --git a/src/index.ts b/src/index.ts index 824c825..b9ac3d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ import { getInput, error, warning, info, debug, setOutput } from '@actions/core'; -import { exec } from 'child_process'; +import { exec, execSync } from 'child_process'; import ms from 'milliseconds'; import kill from 'tree-kill'; @@ -15,6 +15,7 @@ const SHELL = getInput('shell'); const POLLING_INTERVAL_SECONDS = getInputNumber('polling_interval_seconds', false) || 1; const RETRY_ON = getInput('retry_on') || 'any'; const WARNING_ON_RETRY = getInput('warning_on_retry').toLowerCase() === 'true'; +const ON_RETRY_COMMAND = getInput('on_retry_command'); const OS = process.platform; const OUTPUT_TOTAL_ATTEMPTS_KEY = 'total_attempts'; @@ -98,6 +99,19 @@ function getExecutable(): string { return executable } +async function runRetryCmd(): Promise { + // if no retry script, just continue + if (!ON_RETRY_COMMAND) { + return; + } + + try { + await execSync(ON_RETRY_COMMAND, { stdio: 'inherit' }); + } catch (error) { + info(`WARNING: Retry command threw the error ${error.message}`) + } +} + async function runCmd() { const end_time = Date.now() + getTimeout(); const executable = getExecutable(); @@ -164,6 +178,7 @@ async function runAction() { // error: error throw error; } else { + await runRetryCmd(); if (WARNING_ON_RETRY) { warning(`Attempt ${attempt} failed. Reason: ${error.message}`); } else {