mirror of
https://github.com/nick-fields/retry.git
synced 2026-02-10 07:05:29 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c68161adf | ||
|
|
025c480d85 | ||
|
|
3073a9f1e1 | ||
|
|
9e6dab8302 | ||
|
|
850bd83fba | ||
|
|
88ed4273a8 | ||
|
|
bee86ddb77 | ||
|
|
f865f2ade8 | ||
|
|
8310ca5ae8 | ||
|
|
e48877fb9c | ||
|
|
4af9664183 | ||
|
|
d0aac3501c | ||
|
|
877a0ac37e | ||
|
|
7463808b4e | ||
|
|
0aeb89504c | ||
|
|
b2ee390b23 | ||
|
|
fb3bca3fb5 | ||
|
|
51e29ff1ae | ||
|
|
292d515fa9 | ||
|
|
5ee366655c |
73
.github/workflows/ci_cd.yml
vendored
73
.github/workflows/ci_cd.yml
vendored
@@ -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: ./
|
||||
@@ -197,6 +217,59 @@ jobs:
|
||||
expected: failure
|
||||
actual: ${{ steps.sad_path_timeout.outcome }}
|
||||
|
||||
- name: sad-path (wrong shell for OS)
|
||||
id: wrong_shell
|
||||
uses: ./
|
||||
continue-on-error: true
|
||||
with:
|
||||
timeout_minutes: 1
|
||||
max_attempts: 2
|
||||
shell: cmd
|
||||
command: "dir"
|
||||
- uses: nick-invision/assert-action@v1
|
||||
with:
|
||||
expected: 2
|
||||
actual: ${{ steps.wrong_shell.outputs.total_attempts }}
|
||||
- uses: nick-invision/assert-action@v1
|
||||
with:
|
||||
expected: failure
|
||||
actual: ${{ steps.wrong_shell.outcome }}
|
||||
|
||||
ci_windows:
|
||||
name: Run Windows Tests
|
||||
if: startsWith(github.ref, 'refs/heads')
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Powershell test
|
||||
uses: ./
|
||||
with:
|
||||
timeout_minutes: 1
|
||||
max_attempts: 2
|
||||
shell: powershell
|
||||
command: Get-ComputerInfo
|
||||
- name: CMD.exe test
|
||||
uses: ./
|
||||
with:
|
||||
timeout_minutes: 1
|
||||
max_attempts: 2
|
||||
shell: cmd
|
||||
command: echo %PATH%
|
||||
- name: Python test
|
||||
uses: ./
|
||||
with:
|
||||
timeout_minutes: 1
|
||||
max_attempts: 2
|
||||
shell: python
|
||||
command: print('1', '2', '3')
|
||||
|
||||
# runs on push to master only
|
||||
cd:
|
||||
name: Publish Action
|
||||
|
||||
@@ -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 },
|
||||
],
|
||||
},
|
||||
|
||||
40
README.md
40
README.md
@@ -6,7 +6,11 @@ Retries an Action step on failure or timeout. This is currently intended to repl
|
||||
|
||||
### `timeout_minutes`
|
||||
|
||||
**Required** Minutes to wait before attempt times out
|
||||
**Required** Minutes to wait before attempt times out. Must only specify either minutes or seconds
|
||||
|
||||
### `timeout_seconds`
|
||||
|
||||
**Required** Seconds to wait before attempt times out. Must only specify either minutes or seconds
|
||||
|
||||
### `max_attempts`
|
||||
|
||||
@@ -20,6 +24,10 @@ Retries an Action step on failure or timeout. This is currently intended to repl
|
||||
|
||||
**Optional** Number of seconds to wait before attempting the next retry. Defaults to `10`
|
||||
|
||||
### `shell`
|
||||
|
||||
**Optional** Shell to use to execute `command`. Defaults to `powershell` on Windows, `bash` otherwise. Supports bash, python, pwsh, sh, cmd, and powershell per [docs](https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell)
|
||||
|
||||
### `polling_interval_seconds`
|
||||
|
||||
**Optional** Number of seconds to wait while polling for command result. Defaults to `1`
|
||||
@@ -28,6 +36,14 @@ Retries an Action step on failure or timeout. This is currently intended to repl
|
||||
|
||||
**Optional** Event to retry on. Currently supports [any (default), timeout, error].
|
||||
|
||||
### `warning_on_retry`
|
||||
|
||||
**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`
|
||||
@@ -44,6 +60,17 @@ The final error returned by the command
|
||||
|
||||
## Examples
|
||||
|
||||
### Shell
|
||||
|
||||
```yaml
|
||||
uses: nick-invision/retry@v2
|
||||
with:
|
||||
timeout_minutes: 10
|
||||
max_attempts: 3
|
||||
shell: pwsh
|
||||
command: dir
|
||||
```
|
||||
|
||||
### Timeout in minutes
|
||||
|
||||
```yaml
|
||||
@@ -115,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.
|
||||
|
||||
11
action.yml
11
action.yml
@@ -18,12 +18,21 @@ inputs:
|
||||
description: Number of seconds to wait before attempting the next retry
|
||||
required: false
|
||||
default: 10
|
||||
shell:
|
||||
description: Alternate shell to use (defaults to powershell on windows, bash otherwise). Supports bash, python, pwsh, sh, cmd, and powershell
|
||||
required: false
|
||||
polling_interval_seconds:
|
||||
description: Number of seconds to wait for each check that command has completed running
|
||||
required: false
|
||||
default: 1
|
||||
retry_on:
|
||||
description: Event to retry on. Currently supported [any, timeout, error]
|
||||
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
|
||||
@@ -33,4 +42,4 @@ outputs:
|
||||
description: The final error returned by the command
|
||||
runs:
|
||||
using: 'node12'
|
||||
main: 'dist/index.js'
|
||||
main: 'dist/index.js'
|
||||
116
dist/index.js
vendored
116
dist/index.js
vendored
@@ -249,8 +249,12 @@ var TIMEOUT_SECONDS = getInputNumber('timeout_seconds', false);
|
||||
var MAX_ATTEMPTS = getInputNumber('max_attempts', true) || 3;
|
||||
var COMMAND = core_1.getInput('command', { required: true });
|
||||
var RETRY_WAIT_SECONDS = getInputNumber('retry_wait_seconds', false) || 10;
|
||||
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';
|
||||
var OUTPUT_EXIT_ERROR_KEY = 'exit_error';
|
||||
@@ -291,9 +295,6 @@ function validateInputs() {
|
||||
if ((!TIMEOUT_MINUTES && !TIMEOUT_SECONDS) || (TIMEOUT_MINUTES && TIMEOUT_SECONDS)) {
|
||||
throw new Error('Must specify either timeout_minutes or timeout_seconds inputs');
|
||||
}
|
||||
if (TIMEOUT_SECONDS && TIMEOUT_SECONDS < RETRY_WAIT_SECONDS) {
|
||||
throw new Error("timeout_seconds " + TIMEOUT_SECONDS + "s less than retry_wait_seconds " + RETRY_WAIT_SECONDS + "s");
|
||||
}
|
||||
return [2 /*return*/];
|
||||
});
|
||||
});
|
||||
@@ -307,17 +308,78 @@ function getTimeout() {
|
||||
}
|
||||
throw new Error('Must specify either timeout_minutes or timeout_seconds inputs');
|
||||
}
|
||||
function getExecutable() {
|
||||
if (!SHELL) {
|
||||
return OS === 'win32' ? 'powershell' : 'bash';
|
||||
}
|
||||
var executable;
|
||||
switch (SHELL) {
|
||||
case "bash":
|
||||
case "python":
|
||||
case "pwsh": {
|
||||
executable = SHELL;
|
||||
break;
|
||||
}
|
||||
case "sh": {
|
||||
if (OS === 'win32') {
|
||||
throw new Error("Shell " + SHELL + " not allowed on OS " + OS);
|
||||
}
|
||||
executable = SHELL;
|
||||
break;
|
||||
}
|
||||
case "cmd":
|
||||
case "powershell": {
|
||||
if (OS !== 'win32') {
|
||||
throw new Error("Shell " + SHELL + " not allowed on OS " + OS);
|
||||
}
|
||||
executable = SHELL + ".exe";
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error("Shell " + SHELL + " not supported. See https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell for supported shells");
|
||||
}
|
||||
}
|
||||
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 () {
|
||||
var end_time, child;
|
||||
var end_time, executable, child;
|
||||
return __generator(this, function (_c) {
|
||||
switch (_c.label) {
|
||||
case 0:
|
||||
end_time = Date.now() + getTimeout();
|
||||
executable = getExecutable();
|
||||
exit = 0;
|
||||
done = false;
|
||||
child = child_process_1.exec(COMMAND);
|
||||
core_1.debug("Running command " + COMMAND + " on " + OS + " using shell " + executable);
|
||||
child = child_process_1.exec(COMMAND, { 'shell': executable });
|
||||
(_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on('data', function (data) {
|
||||
process.stdout.write(data);
|
||||
});
|
||||
@@ -364,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()];
|
||||
@@ -373,38 +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 {
|
||||
core_1.warning("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*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
2119
package-lock.json
generated
2119
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -32,7 +32,7 @@
|
||||
"@zeit/ncc": "^0.20.5",
|
||||
"dotenv": "8.2.0",
|
||||
"husky": "4.3.0",
|
||||
"semantic-release": "17.2.2",
|
||||
"semantic-release": "17.2.3",
|
||||
"ts-node": "9.0.0",
|
||||
"typescript": "4.0.5"
|
||||
},
|
||||
|
||||
@@ -2,5 +2,6 @@ INPUT_TIMEOUT_MINUTES=1
|
||||
INPUT_MAX_ATTEMPTS=3
|
||||
INPUT_COMMAND="node -e 'process.exit(99)'"
|
||||
INPUT_RETRY_WAIT_SECONDS=10
|
||||
SHELL=pwsh
|
||||
INPUT_POLLING_INTERVAL_SECONDS=1
|
||||
INPUT_RETRY_ON=any
|
||||
|
||||
71
src/index.ts
71
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';
|
||||
|
||||
@@ -11,9 +11,13 @@ const TIMEOUT_SECONDS = getInputNumber('timeout_seconds', false);
|
||||
const MAX_ATTEMPTS = getInputNumber('max_attempts', true) || 3;
|
||||
const COMMAND = getInput('command', { required: true });
|
||||
const RETRY_WAIT_SECONDS = getInputNumber('retry_wait_seconds', false) || 10;
|
||||
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';
|
||||
const OUTPUT_EXIT_CODE_KEY = 'exit_code';
|
||||
const OUTPUT_EXIT_ERROR_KEY = 'exit_error';
|
||||
@@ -48,12 +52,6 @@ async function validateInputs() {
|
||||
if ((!TIMEOUT_MINUTES && !TIMEOUT_SECONDS) || (TIMEOUT_MINUTES && TIMEOUT_SECONDS)) {
|
||||
throw new Error('Must specify either timeout_minutes or timeout_seconds inputs');
|
||||
}
|
||||
|
||||
if (TIMEOUT_SECONDS && TIMEOUT_SECONDS < RETRY_WAIT_SECONDS) {
|
||||
throw new Error(
|
||||
`timeout_seconds ${TIMEOUT_SECONDS}s less than retry_wait_seconds ${RETRY_WAIT_SECONDS}s`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function getTimeout(): number {
|
||||
@@ -66,13 +64,63 @@ function getTimeout(): number {
|
||||
throw new Error('Must specify either timeout_minutes or timeout_seconds inputs');
|
||||
}
|
||||
|
||||
function getExecutable(): string {
|
||||
if (!SHELL) {
|
||||
return OS === 'win32' ? 'powershell' : 'bash';
|
||||
}
|
||||
|
||||
let executable: string;
|
||||
switch (SHELL) {
|
||||
case "bash":
|
||||
case "python":
|
||||
case "pwsh": {
|
||||
executable = SHELL;
|
||||
break;
|
||||
}
|
||||
case "sh": {
|
||||
if (OS === 'win32') {
|
||||
throw new Error(`Shell ${SHELL} not allowed on OS ${OS}`);
|
||||
}
|
||||
executable = SHELL;
|
||||
break;
|
||||
}
|
||||
case "cmd":
|
||||
case "powershell": {
|
||||
if (OS !== 'win32') {
|
||||
throw new Error(`Shell ${SHELL} not allowed on OS ${OS}`);
|
||||
}
|
||||
executable = SHELL + ".exe";
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Shell ${SHELL} not supported. See https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell for supported shells`);
|
||||
}
|
||||
}
|
||||
return executable
|
||||
}
|
||||
|
||||
async function runRetryCmd(): Promise<void> {
|
||||
// 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();
|
||||
|
||||
exit = 0;
|
||||
done = false;
|
||||
|
||||
var child = exec(COMMAND);
|
||||
debug(`Running command ${COMMAND} on ${OS} using shell ${executable}`)
|
||||
var child = exec(COMMAND, { 'shell': executable });
|
||||
|
||||
child.stdout?.on('data', (data) => {
|
||||
process.stdout.write(data);
|
||||
@@ -130,7 +178,12 @@ async function runAction() {
|
||||
// error: error
|
||||
throw error;
|
||||
} else {
|
||||
warning(`Attempt ${attempt} failed. Reason: ${error.message}`);
|
||||
await runRetryCmd();
|
||||
if (WARNING_ON_RETRY) {
|
||||
warning(`Attempt ${attempt} failed. Reason: ${error.message}`);
|
||||
} else {
|
||||
info(`Attempt ${attempt} failed. Reason: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user