mirror of
https://github.com/nick-fields/retry.git
synced 2026-02-10 07:05:29 +00:00
Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c803451cc1 | ||
|
|
3f5463b526 | ||
|
|
915303cda5 | ||
|
|
86ecaf34fa | ||
|
|
ec785f59e1 | ||
|
|
d2b20569e3 | ||
|
|
7841cadab1 | ||
|
|
87ec0a8a32 | ||
|
|
fc84966019 | ||
|
|
39da88d5f7 | ||
|
|
6a380b501f | ||
|
|
3ded872743 | ||
|
|
88ea919f23 |
159
.github/workflows/ci_cd.yml
vendored
159
.github/workflows/ci_cd.yml
vendored
@@ -18,33 +18,178 @@ jobs:
|
|||||||
node-version: 12
|
node-version: 12
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
- name: Test
|
|
||||||
uses: ./
|
|
||||||
continue-on-error: true
|
|
||||||
with:
|
|
||||||
timeout_minutes: 1
|
|
||||||
max_attempts: 3
|
|
||||||
command: npm install this-isnt-a-real-package-name-zzz
|
|
||||||
- name: happy-path
|
- name: happy-path
|
||||||
|
id: happy_path
|
||||||
uses: ./
|
uses: ./
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 1
|
timeout_minutes: 1
|
||||||
max_attempts: 2
|
max_attempts: 2
|
||||||
command: npm -v
|
command: npm -v
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: true
|
||||||
|
actual: ${{ steps.happy_path.outputs.total_attempts == '1' && steps.happy_path.outputs.exit_code == '0' }}
|
||||||
|
|
||||||
|
- name: sad-path (retry_wait_seconds)
|
||||||
|
id: sad_path_wait_sec
|
||||||
|
uses: ./
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
timeout_minutes: 1
|
||||||
|
max_attempts: 3
|
||||||
|
retry_wait_seconds: 15
|
||||||
|
command: npm install this-isnt-a-real-package-name-zzz
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 3
|
||||||
|
actual: ${{ steps.sad_path_wait_sec.outputs.total_attempts }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.sad_path_wait_sec.outcome }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 'Final attempt failed'
|
||||||
|
actual: ${{ steps.sad_path_wait_sec.outputs.exit_error }}
|
||||||
|
comparison: contains
|
||||||
|
|
||||||
- name: sad-path (error)
|
- name: sad-path (error)
|
||||||
|
id: sad_path_error
|
||||||
uses: ./
|
uses: ./
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 1
|
timeout_minutes: 1
|
||||||
max_attempts: 2
|
max_attempts: 2
|
||||||
command: node -e "process.exit(1)"
|
command: node -e "process.exit(1)"
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 2
|
||||||
|
actual: ${{ steps.sad_path_error.outputs.total_attempts }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.sad_path_error.outcome }}
|
||||||
|
|
||||||
|
- name: retry_on (timeout) fails early if error encountered
|
||||||
|
id: retry_on_timeout_fail
|
||||||
|
uses: ./
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
timeout_minutes: 1
|
||||||
|
max_attempts: 3
|
||||||
|
retry_on: timeout
|
||||||
|
command: node -e "process.exit(2)"
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 1
|
||||||
|
actual: ${{ steps.retry_on_timeout_fail.outputs.total_attempts }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.retry_on_timeout_fail.outcome }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 2
|
||||||
|
actual: ${{ steps.retry_on_timeout_fail.outputs.exit_code }}
|
||||||
|
|
||||||
|
- name: retry_on (error)
|
||||||
|
id: retry_on_error
|
||||||
|
uses: ./
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
timeout_minutes: 1
|
||||||
|
max_attempts: 2
|
||||||
|
retry_on: error
|
||||||
|
command: node -e "process.exit(2)"
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 2
|
||||||
|
actual: ${{ steps.retry_on_error.outputs.total_attempts }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.retry_on_error.outcome }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 2
|
||||||
|
actual: ${{ steps.retry_on_error.outputs.exit_code }}
|
||||||
|
|
||||||
|
|
||||||
|
# timeout tests (takes longer to run so run last)
|
||||||
- name: sad-path (timeout)
|
- name: sad-path (timeout)
|
||||||
|
id: sad_path_timeout
|
||||||
|
uses: ./
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
timeout_seconds: 15
|
||||||
|
max_attempts: 2
|
||||||
|
command: node -e "(async()=>await new Promise(r => setTimeout(r, 120000)))()"
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 2
|
||||||
|
actual: ${{ steps.sad_path_timeout.outputs.total_attempts }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.sad_path_timeout.outcome }}
|
||||||
|
|
||||||
|
- name: retry_on (timeout)
|
||||||
|
id: retry_on_timeout
|
||||||
|
uses: ./
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
timeout_seconds: 15
|
||||||
|
max_attempts: 2
|
||||||
|
retry_on: timeout
|
||||||
|
command: node -e "(async()=>await new Promise(r => setTimeout(r, 120000)))()"
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 2
|
||||||
|
actual: ${{ steps.retry_on_timeout.outputs.total_attempts }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.retry_on_timeout.outcome }}
|
||||||
|
|
||||||
|
- name: retry_on (error) fails early if timeout encountered
|
||||||
|
id: retry_on_error_fail
|
||||||
|
uses: ./
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
timeout_seconds: 15
|
||||||
|
max_attempts: 2
|
||||||
|
retry_on: error
|
||||||
|
command: node -e "(async()=>await new Promise(r => setTimeout(r, 120000)))()"
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 1
|
||||||
|
actual: ${{ steps.retry_on_error_fail.outputs.total_attempts }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.retry_on_error_fail.outcome }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 1
|
||||||
|
actual: ${{ steps.retry_on_error_fail.outputs.exit_code }}
|
||||||
|
|
||||||
|
- name: sad-path (timeout minutes)
|
||||||
|
id: sad_path_timeout_minutes
|
||||||
uses: ./
|
uses: ./
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 1
|
timeout_minutes: 1
|
||||||
max_attempts: 2
|
max_attempts: 2
|
||||||
command: node -e "(async()=>await new Promise(r => setTimeout(r, 120000)))()"
|
command: node -e "(async()=>await new Promise(r => setTimeout(r, 120000)))()"
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 2
|
||||||
|
actual: ${{ steps.sad_path_timeout.outputs.total_attempts }}
|
||||||
|
- uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.sad_path_timeout.outcome }}
|
||||||
|
|
||||||
# runs on push to master only
|
# runs on push to master only
|
||||||
cd:
|
cd:
|
||||||
|
|||||||
91
README.md
91
README.md
@@ -24,12 +24,97 @@ Retries an Action step on failure or timeout. This is currently intended to repl
|
|||||||
|
|
||||||
**Optional** Number of seconds to wait while polling for command result. Defaults to `1`
|
**Optional** Number of seconds to wait while polling for command result. Defaults to `1`
|
||||||
|
|
||||||
## Example usage
|
### `retry_on`
|
||||||
|
|
||||||
|
**Optional** Event to retry on. Currently supports [any (default), timeout, error].
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
### `total_attempts`
|
||||||
|
|
||||||
|
The final number of attempts made
|
||||||
|
|
||||||
|
### `exit_code`
|
||||||
|
|
||||||
|
The final exit code returned by the command
|
||||||
|
|
||||||
|
### `exit_error`
|
||||||
|
|
||||||
|
The final error returned by the command
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Timeout in minutes
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
uses: nick-invision/retry@v1
|
uses: nick-invision/retry@v2
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 10
|
timeout_minutes: 10
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
command: npm install
|
command: npm run some-typically-slow-script
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Timeout in seconds
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: nick-invision/retry@v2
|
||||||
|
with:
|
||||||
|
timeout_seconds: 15
|
||||||
|
max_attempts: 3
|
||||||
|
command: npm run some-typically-fast-script
|
||||||
|
```
|
||||||
|
|
||||||
|
### Only retry after timeout
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: nick-invision/retry@v2
|
||||||
|
with:
|
||||||
|
timeout_seconds: 15
|
||||||
|
max_attempts: 3
|
||||||
|
retry_on: timeout
|
||||||
|
command: npm run some-typically-fast-script
|
||||||
|
```
|
||||||
|
|
||||||
|
### Only retry after error
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: nick-invision/retry@v2
|
||||||
|
with:
|
||||||
|
timeout_seconds: 15
|
||||||
|
max_attempts: 3
|
||||||
|
retry_on: error
|
||||||
|
command: npm run some-typically-fast-script
|
||||||
|
```
|
||||||
|
|
||||||
|
### Retry but allow failure and do something with output
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- uses: nick-invision/retry@v2
|
||||||
|
id: retry
|
||||||
|
# see https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#jobsjob_idcontinue-on-error
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
timeout_seconds: 15
|
||||||
|
max_attempts: 3
|
||||||
|
retry_on: error
|
||||||
|
command: node -e 'process.exit(99);'
|
||||||
|
- name: Assert that action failed
|
||||||
|
uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: failure
|
||||||
|
actual: ${{ steps.retry.outcome }}
|
||||||
|
- name: Assert that action exited with expected exit code
|
||||||
|
uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 99
|
||||||
|
actual: ${{ steps.retry.outputs.exit_code }}
|
||||||
|
- name: Assert that action made expected number of attempts
|
||||||
|
uses: nick-invision/assert-action@v1
|
||||||
|
with:
|
||||||
|
expected: 3
|
||||||
|
actual: ${{ steps.retry.outputs.total_attempts }}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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.
|
||||||
|
|||||||
18
action.yml
18
action.yml
@@ -2,8 +2,11 @@ name: Retry Step
|
|||||||
description: 'Retry a step on failure or timeout'
|
description: 'Retry a step on failure or timeout'
|
||||||
inputs:
|
inputs:
|
||||||
timeout_minutes:
|
timeout_minutes:
|
||||||
description: Minutes to wait before attempt times out
|
description: Minutes to wait before attempt times out. Must only specify either minutes or seconds
|
||||||
required: true
|
required: false
|
||||||
|
timeout_seconds:
|
||||||
|
description: Seconds to wait before attempt times out. Must only specify either minutes or seconds
|
||||||
|
required: false
|
||||||
max_attempts:
|
max_attempts:
|
||||||
description: Number of attempts to make before failing the step
|
description: Number of attempts to make before failing the step
|
||||||
required: true
|
required: true
|
||||||
@@ -19,6 +22,15 @@ inputs:
|
|||||||
description: Number of seconds to wait for each check that command has completed running
|
description: Number of seconds to wait for each check that command has completed running
|
||||||
required: false
|
required: false
|
||||||
default: 1
|
default: 1
|
||||||
|
retry_on:
|
||||||
|
description: Event to retry on. Currently supported [any, timeout, error]
|
||||||
|
outputs:
|
||||||
|
total_attempts:
|
||||||
|
description: The final number of attempts made
|
||||||
|
exit_code:
|
||||||
|
description: The final exit code returned by the command
|
||||||
|
exit_error:
|
||||||
|
description: The final error returned by the command
|
||||||
runs:
|
runs:
|
||||||
using: 'node12'
|
using: 'node12'
|
||||||
main: 'dist/index.js'
|
main: 'dist/index.js'
|
||||||
|
|||||||
8
dist/exec.js
vendored
8
dist/exec.js
vendored
@@ -1,8 +1,12 @@
|
|||||||
const { execSync } = require('child_process');
|
const { exec } = require('child_process');
|
||||||
const COMMAND = process.argv.splice(2)[0];
|
const COMMAND = process.argv.splice(2)[0];
|
||||||
|
|
||||||
function run() {
|
function run() {
|
||||||
execSync(COMMAND, { stdio: 'inherit' });
|
exec(COMMAND, { stdio: 'inherit' }, (err) => {
|
||||||
|
if (err) {
|
||||||
|
process.exit(err.code);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
run();
|
||||||
|
|||||||
169
dist/index.js
vendored
169
dist/index.js
vendored
@@ -143,14 +143,28 @@ class Command {
|
|||||||
return cmdStr;
|
return cmdStr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Sanitizes an input into a string so it can be passed into issueCommand safely
|
||||||
|
* @param input input to sanitize into a string
|
||||||
|
*/
|
||||||
|
function toCommandValue(input) {
|
||||||
|
if (input === null || input === undefined) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
else if (typeof input === 'string' || input instanceof String) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
return JSON.stringify(input);
|
||||||
|
}
|
||||||
|
exports.toCommandValue = toCommandValue;
|
||||||
function escapeData(s) {
|
function escapeData(s) {
|
||||||
return (s || '')
|
return toCommandValue(s)
|
||||||
.replace(/%/g, '%25')
|
.replace(/%/g, '%25')
|
||||||
.replace(/\r/g, '%0D')
|
.replace(/\r/g, '%0D')
|
||||||
.replace(/\n/g, '%0A');
|
.replace(/\n/g, '%0A');
|
||||||
}
|
}
|
||||||
function escapeProperty(s) {
|
function escapeProperty(s) {
|
||||||
return (s || '')
|
return toCommandValue(s)
|
||||||
.replace(/%/g, '%25')
|
.replace(/%/g, '%25')
|
||||||
.replace(/\r/g, '%0D')
|
.replace(/\r/g, '%0D')
|
||||||
.replace(/\n/g, '%0A')
|
.replace(/\n/g, '%0A')
|
||||||
@@ -206,11 +220,13 @@ var ExitCode;
|
|||||||
/**
|
/**
|
||||||
* Sets env variable for this action and future actions in the job
|
* Sets env variable for this action and future actions in the job
|
||||||
* @param name the name of the variable to set
|
* @param name the name of the variable to set
|
||||||
* @param val the value of the variable
|
* @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function exportVariable(name, val) {
|
function exportVariable(name, val) {
|
||||||
process.env[name] = val;
|
const convertedVal = command_1.toCommandValue(val);
|
||||||
command_1.issueCommand('set-env', { name }, val);
|
process.env[name] = convertedVal;
|
||||||
|
command_1.issueCommand('set-env', { name }, convertedVal);
|
||||||
}
|
}
|
||||||
exports.exportVariable = exportVariable;
|
exports.exportVariable = exportVariable;
|
||||||
/**
|
/**
|
||||||
@@ -249,12 +265,22 @@ exports.getInput = getInput;
|
|||||||
* Sets the value of an output.
|
* Sets the value of an output.
|
||||||
*
|
*
|
||||||
* @param name name of the output to set
|
* @param name name of the output to set
|
||||||
* @param value value to store
|
* @param value value to store. Non-string values will be converted to a string via JSON.stringify
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function setOutput(name, value) {
|
function setOutput(name, value) {
|
||||||
command_1.issueCommand('set-output', { name }, value);
|
command_1.issueCommand('set-output', { name }, value);
|
||||||
}
|
}
|
||||||
exports.setOutput = setOutput;
|
exports.setOutput = setOutput;
|
||||||
|
/**
|
||||||
|
* Enables or disables the echoing of commands into stdout for the rest of the step.
|
||||||
|
* Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
function setCommandEcho(enabled) {
|
||||||
|
command_1.issue('echo', enabled ? 'on' : 'off');
|
||||||
|
}
|
||||||
|
exports.setCommandEcho = setCommandEcho;
|
||||||
//-----------------------------------------------------------------------
|
//-----------------------------------------------------------------------
|
||||||
// Results
|
// Results
|
||||||
//-----------------------------------------------------------------------
|
//-----------------------------------------------------------------------
|
||||||
@@ -271,6 +297,13 @@ exports.setFailed = setFailed;
|
|||||||
//-----------------------------------------------------------------------
|
//-----------------------------------------------------------------------
|
||||||
// Logging Commands
|
// Logging Commands
|
||||||
//-----------------------------------------------------------------------
|
//-----------------------------------------------------------------------
|
||||||
|
/**
|
||||||
|
* Gets whether Actions Step Debug is on or not
|
||||||
|
*/
|
||||||
|
function isDebug() {
|
||||||
|
return process.env['RUNNER_DEBUG'] === '1';
|
||||||
|
}
|
||||||
|
exports.isDebug = isDebug;
|
||||||
/**
|
/**
|
||||||
* Writes debug message to user log
|
* Writes debug message to user log
|
||||||
* @param message debug message
|
* @param message debug message
|
||||||
@@ -281,18 +314,18 @@ function debug(message) {
|
|||||||
exports.debug = debug;
|
exports.debug = debug;
|
||||||
/**
|
/**
|
||||||
* Adds an error issue
|
* Adds an error issue
|
||||||
* @param message error issue message
|
* @param message error issue message. Errors will be converted to string via toString()
|
||||||
*/
|
*/
|
||||||
function error(message) {
|
function error(message) {
|
||||||
command_1.issue('error', message);
|
command_1.issue('error', message instanceof Error ? message.toString() : message);
|
||||||
}
|
}
|
||||||
exports.error = error;
|
exports.error = error;
|
||||||
/**
|
/**
|
||||||
* Adds an warning issue
|
* Adds an warning issue
|
||||||
* @param message warning issue message
|
* @param message warning issue message. Errors will be converted to string via toString()
|
||||||
*/
|
*/
|
||||||
function warning(message) {
|
function warning(message) {
|
||||||
command_1.issue('warning', message);
|
command_1.issue('warning', message instanceof Error ? message.toString() : message);
|
||||||
}
|
}
|
||||||
exports.warning = warning;
|
exports.warning = warning;
|
||||||
/**
|
/**
|
||||||
@@ -350,8 +383,9 @@ exports.group = group;
|
|||||||
* Saves state for current action, the state can only be retrieved by this action's post job execution.
|
* Saves state for current action, the state can only be retrieved by this action's post job execution.
|
||||||
*
|
*
|
||||||
* @param name name of the state to store
|
* @param name name of the state to store
|
||||||
* @param value value to store
|
* @param value value to store. Non-string values will be converted to a string via JSON.stringify
|
||||||
*/
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
function saveState(name, value) {
|
function saveState(name, value) {
|
||||||
command_1.issueCommand('save-state', { name }, value);
|
command_1.issueCommand('save-state', { name }, value);
|
||||||
}
|
}
|
||||||
@@ -380,16 +414,37 @@ module.exports = require("path");
|
|||||||
/***/ 676:
|
/***/ 676:
|
||||||
/***/ (function(__unusedmodule, __unusedexports, __webpack_require__) {
|
/***/ (function(__unusedmodule, __unusedexports, __webpack_require__) {
|
||||||
|
|
||||||
const { getInput, error, warning, info } = __webpack_require__(470);
|
const { getInput, error, warning, info, debug, setOutput } = __webpack_require__(470);
|
||||||
const { spawn } = __webpack_require__(129);
|
const { spawn } = __webpack_require__(129);
|
||||||
const { join } = __webpack_require__(622);
|
const { join } = __webpack_require__(622);
|
||||||
const ms = __webpack_require__(156);
|
const ms = __webpack_require__(156);
|
||||||
var kill = __webpack_require__(791);
|
var kill = __webpack_require__(791);
|
||||||
|
|
||||||
|
// inputs
|
||||||
|
const TIMEOUT_MINUTES = getInputNumber('timeout_minutes', false);
|
||||||
|
const TIMEOUT_SECONDS = getInputNumber('timeout_seconds', false);
|
||||||
|
const MAX_ATTEMPTS = getInputNumber('max_attempts', true);
|
||||||
|
const COMMAND = getInput('command', { required: true });
|
||||||
|
const RETRY_WAIT_SECONDS = getInputNumber('retry_wait_seconds', false);
|
||||||
|
const POLLING_INTERVAL_SECONDS = getInputNumber('polling_interval_seconds', false);
|
||||||
|
const RETRY_ON = getInput('retry_on') || 'any';
|
||||||
|
|
||||||
|
const OUTPUT_TOTAL_ATTEMPTS_KEY = 'total_attempts';
|
||||||
|
const OUTPUT_EXIT_CODE_KEY = 'exit_code';
|
||||||
|
const OUTPUT_EXIT_ERROR_KEY = 'exit_error';
|
||||||
|
|
||||||
|
var exit;
|
||||||
|
var done;
|
||||||
|
|
||||||
function getInputNumber(id, required) {
|
function getInputNumber(id, required) {
|
||||||
const input = getInput(id, { required });
|
const input = getInput(id, { required });
|
||||||
const num = Number.parseInt(input);
|
const num = Number.parseInt(input);
|
||||||
|
|
||||||
|
// empty is ok
|
||||||
|
if (!input && !required) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!Number.isInteger(num)) {
|
if (!Number.isInteger(num)) {
|
||||||
throw `Input ${id} only accepts numbers. Received ${input}`;
|
throw `Input ${id} only accepts numbers. Received ${input}`;
|
||||||
}
|
}
|
||||||
@@ -397,65 +452,115 @@ function getInputNumber(id, required) {
|
|||||||
return num;
|
return num;
|
||||||
}
|
}
|
||||||
|
|
||||||
// inputs
|
|
||||||
const TIMEOUT_MINUTES = getInputNumber('timeout_minutes', true);
|
|
||||||
const MAX_ATTEMPTS = getInputNumber('max_attempts', true);
|
|
||||||
const COMMAND = getInput('command', { required: true });
|
|
||||||
const RETRY_WAIT_SECONDS = getInputNumber('retry_wait_seconds', false);
|
|
||||||
const POLLING_INTERVAL_SECONDS = getInputNumber('polling_interval_seconds', false);
|
|
||||||
|
|
||||||
async function wait(ms) {
|
async function wait(ms) {
|
||||||
return new Promise(r => setTimeout(r, ms));
|
return new Promise((r) => setTimeout(r, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function retryWait() {
|
||||||
|
const waitStart = Date.now();
|
||||||
|
await wait(ms.seconds(RETRY_WAIT_SECONDS));
|
||||||
|
debug(`Waited ${Date.now() - waitStart}ms`);
|
||||||
|
debug(`Configured wait: ${ms.seconds(RETRY_WAIT_SECONDS)}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
if (TIMEOUT_MINUTES) {
|
||||||
|
return ms.minutes(TIMEOUT_MINUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ms.seconds(TIMEOUT_SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runCmd() {
|
async function runCmd() {
|
||||||
const end_time = Date.now() + ms.minutes(TIMEOUT_MINUTES);
|
const end_time = Date.now() + getTimeout();
|
||||||
var done, exit;
|
|
||||||
|
exit = 0;
|
||||||
|
done = false;
|
||||||
|
|
||||||
var child = spawn('node', [__webpack_require__.ab + "exec.js", COMMAND], { stdio: 'inherit' });
|
var child = spawn('node', [__webpack_require__.ab + "exec.js", COMMAND], { stdio: 'inherit' });
|
||||||
|
|
||||||
child.on('exit', code => {
|
child.on('exit', (code, signal) => {
|
||||||
|
debug(`Code: ${code}`);
|
||||||
|
debug(`Signal: ${signal}`);
|
||||||
if (code > 0) {
|
if (code > 0) {
|
||||||
exit = code;
|
exit = code;
|
||||||
}
|
}
|
||||||
|
// timeouts are killed manually
|
||||||
|
if (signal === 'SIGTERM') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
done = true;
|
done = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
do {
|
do {
|
||||||
await wait(ms.seconds(POLLING_INTERVAL_SECONDS));
|
await wait(ms.seconds(POLLING_INTERVAL_SECONDS));
|
||||||
} while (Date.now() < end_time && !done && !exit);
|
} while (Date.now() < end_time && !done);
|
||||||
|
|
||||||
if (!done) {
|
if (!done) {
|
||||||
kill(child.pid);
|
kill(child.pid);
|
||||||
await wait(ms.seconds(RETRY_WAIT_SECONDS));
|
await retryWait();
|
||||||
throw new Error(`Timeout of ${TIMEOUT_MINUTES}m hit`);
|
throw new Error(`Timeout of ${getTimeout()}ms hit`);
|
||||||
} else if (exit > 0) {
|
} else if (exit > 0) {
|
||||||
throw new Error(`Child_process exited with error`);
|
await retryWait();
|
||||||
|
throw new Error(`Child_process exited with error code ${exit}`);
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runAction() {
|
async function runAction() {
|
||||||
|
await validateInputs();
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
||||||
try {
|
try {
|
||||||
|
// just keep overwriting attempts output
|
||||||
|
setOutput(OUTPUT_TOTAL_ATTEMPTS_KEY, attempt);
|
||||||
await runCmd();
|
await runCmd();
|
||||||
info(`Command completed after ${attempt} attempt(s).`);
|
info(`Command completed after ${attempt} attempt(s).`);
|
||||||
break;
|
break;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (attempt === MAX_ATTEMPTS) {
|
if (attempt === MAX_ATTEMPTS) {
|
||||||
throw new Error(`Final attempt failed. ${error.message}`);
|
throw new Error(`Final attempt failed. ${error.message}`);
|
||||||
|
} else if (!done && RETRY_ON === 'error') {
|
||||||
|
// error: timeout
|
||||||
|
throw error;
|
||||||
|
} else if (exit > 0 && RETRY_ON === 'timeout') {
|
||||||
|
// error: error
|
||||||
|
throw error;
|
||||||
} else {
|
} else {
|
||||||
warning(`Attempt ${attempt} failed. Reason:`, error.message);
|
warning(`Attempt ${attempt} failed. Reason: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
runAction().catch(err => {
|
runAction()
|
||||||
error(err.message);
|
.then(() => {
|
||||||
process.exit(1);
|
setOutput(OUTPUT_EXIT_CODE_KEY, 0);
|
||||||
});
|
process.exit(0); // success
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error(err.message);
|
||||||
|
|
||||||
|
// these can be helpful to know if continue-on-error is true
|
||||||
|
setOutput(OUTPUT_EXIT_ERROR_KEY, err.message);
|
||||||
|
setOutput(OUTPUT_EXIT_CODE_KEY, exit > 0 ? exit : 1);
|
||||||
|
|
||||||
|
// exit with exact error code if available, otherwise just exit with 1
|
||||||
|
process.exit(exit > 0 ? exit : 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|||||||
110
package-lock.json
generated
110
package-lock.json
generated
@@ -1501,6 +1501,12 @@
|
|||||||
"is-obj": "^1.0.0"
|
"is-obj": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dotenv": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz",
|
||||||
|
"integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"duplexer2": {
|
"duplexer2": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
|
||||||
@@ -2573,9 +2579,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"npm": {
|
"npm": {
|
||||||
"version": "6.13.7",
|
"version": "6.14.6",
|
||||||
"resolved": "https://registry.npmjs.org/npm/-/npm-6.13.7.tgz",
|
"resolved": "https://registry.npmjs.org/npm/-/npm-6.14.6.tgz",
|
||||||
"integrity": "sha512-X967EKTT407CvgrWFjXusnPh0VLERcmR9hZFSVgkEquOomZkvpwLJ5zrQ3qrG9SpPLKJE4bPLUu76exKQ4a3Cg==",
|
"integrity": "sha512-axnz6iHFK6WPE0js/+mRp+4IOwpHn5tJEw5KB6FiCU764zmffrhsYHbSHi2kKqNkRBt53XasXjngZfBD3FQzrQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"JSONStream": "^1.3.5",
|
"JSONStream": "^1.3.5",
|
||||||
@@ -2589,7 +2595,7 @@
|
|||||||
"byte-size": "^5.0.1",
|
"byte-size": "^5.0.1",
|
||||||
"cacache": "^12.0.3",
|
"cacache": "^12.0.3",
|
||||||
"call-limit": "^1.1.1",
|
"call-limit": "^1.1.1",
|
||||||
"chownr": "^1.1.3",
|
"chownr": "^1.1.4",
|
||||||
"ci-info": "^2.0.0",
|
"ci-info": "^2.0.0",
|
||||||
"cli-columns": "^3.1.2",
|
"cli-columns": "^3.1.2",
|
||||||
"cli-table3": "^0.5.1",
|
"cli-table3": "^0.5.1",
|
||||||
@@ -2606,10 +2612,10 @@
|
|||||||
"fs-vacuum": "~1.2.10",
|
"fs-vacuum": "~1.2.10",
|
||||||
"fs-write-stream-atomic": "~1.0.10",
|
"fs-write-stream-atomic": "~1.0.10",
|
||||||
"gentle-fs": "^2.3.0",
|
"gentle-fs": "^2.3.0",
|
||||||
"glob": "^7.1.4",
|
"glob": "^7.1.6",
|
||||||
"graceful-fs": "^4.2.3",
|
"graceful-fs": "^4.2.4",
|
||||||
"has-unicode": "~2.0.1",
|
"has-unicode": "~2.0.1",
|
||||||
"hosted-git-info": "^2.8.5",
|
"hosted-git-info": "^2.8.8",
|
||||||
"iferr": "^1.0.2",
|
"iferr": "^1.0.2",
|
||||||
"imurmurhash": "*",
|
"imurmurhash": "*",
|
||||||
"infer-owner": "^1.0.4",
|
"infer-owner": "^1.0.4",
|
||||||
@@ -2644,20 +2650,20 @@
|
|||||||
"lru-cache": "^5.1.1",
|
"lru-cache": "^5.1.1",
|
||||||
"meant": "~1.0.1",
|
"meant": "~1.0.1",
|
||||||
"mississippi": "^3.0.0",
|
"mississippi": "^3.0.0",
|
||||||
"mkdirp": "~0.5.1",
|
"mkdirp": "^0.5.5",
|
||||||
"move-concurrently": "^1.0.1",
|
"move-concurrently": "^1.0.1",
|
||||||
"node-gyp": "^5.0.7",
|
"node-gyp": "^5.1.0",
|
||||||
"nopt": "~4.0.1",
|
"nopt": "^4.0.3",
|
||||||
"normalize-package-data": "^2.5.0",
|
"normalize-package-data": "^2.5.0",
|
||||||
"npm-audit-report": "^1.3.2",
|
"npm-audit-report": "^1.3.2",
|
||||||
"npm-cache-filename": "~1.0.2",
|
"npm-cache-filename": "~1.0.2",
|
||||||
"npm-install-checks": "^3.0.2",
|
"npm-install-checks": "^3.0.2",
|
||||||
"npm-lifecycle": "^3.1.4",
|
"npm-lifecycle": "^3.1.4",
|
||||||
"npm-package-arg": "^6.1.1",
|
"npm-package-arg": "^6.1.1",
|
||||||
"npm-packlist": "^1.4.7",
|
"npm-packlist": "^1.4.8",
|
||||||
"npm-pick-manifest": "^3.0.2",
|
"npm-pick-manifest": "^3.0.2",
|
||||||
"npm-profile": "^4.0.2",
|
"npm-profile": "^4.0.4",
|
||||||
"npm-registry-fetch": "^4.0.2",
|
"npm-registry-fetch": "^4.0.5",
|
||||||
"npm-user-validate": "~1.0.0",
|
"npm-user-validate": "~1.0.0",
|
||||||
"npmlog": "~4.1.2",
|
"npmlog": "~4.1.2",
|
||||||
"once": "~1.4.0",
|
"once": "~1.4.0",
|
||||||
@@ -2674,11 +2680,11 @@
|
|||||||
"read-installed": "~4.0.3",
|
"read-installed": "~4.0.3",
|
||||||
"read-package-json": "^2.1.1",
|
"read-package-json": "^2.1.1",
|
||||||
"read-package-tree": "^5.3.1",
|
"read-package-tree": "^5.3.1",
|
||||||
"readable-stream": "^3.4.0",
|
"readable-stream": "^3.6.0",
|
||||||
"readdir-scoped-modules": "^1.1.0",
|
"readdir-scoped-modules": "^1.1.0",
|
||||||
"request": "^2.88.0",
|
"request": "^2.88.0",
|
||||||
"retry": "^0.12.0",
|
"retry": "^0.12.0",
|
||||||
"rimraf": "^2.6.3",
|
"rimraf": "^2.7.1",
|
||||||
"safe-buffer": "^5.1.2",
|
"safe-buffer": "^5.1.2",
|
||||||
"semver": "^5.7.1",
|
"semver": "^5.7.1",
|
||||||
"sha": "^3.0.0",
|
"sha": "^3.0.0",
|
||||||
@@ -2979,7 +2985,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"chownr": {
|
"chownr": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
@@ -3285,7 +3291,7 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"deep-extend": {
|
"deep-extend": {
|
||||||
"version": "0.5.1",
|
"version": "0.6.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
@@ -3784,7 +3790,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"glob": {
|
"glob": {
|
||||||
"version": "7.1.4",
|
"version": "7.1.6",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -3830,7 +3836,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"graceful-fs": {
|
"graceful-fs": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
@@ -3872,7 +3878,7 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"hosted-git-info": {
|
"hosted-git-info": {
|
||||||
"version": "2.8.5",
|
"version": "2.8.8",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
@@ -4008,11 +4014,11 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"is-ci": {
|
"is-ci": {
|
||||||
"version": "1.1.0",
|
"version": "1.2.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ci-info": "^1.0.0"
|
"ci-info": "^1.5.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ci-info": {
|
"ci-info": {
|
||||||
@@ -4084,7 +4090,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"is-retry-allowed": {
|
"is-retry-allowed": {
|
||||||
"version": "1.1.0",
|
"version": "1.2.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
@@ -4556,11 +4562,6 @@
|
|||||||
"brace-expansion": "^1.1.7"
|
"brace-expansion": "^1.1.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"minimist": {
|
|
||||||
"version": "0.0.8",
|
|
||||||
"bundled": true,
|
|
||||||
"dev": true
|
|
||||||
},
|
|
||||||
"minizlib": {
|
"minizlib": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
@@ -4598,11 +4599,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mkdirp": {
|
"mkdirp": {
|
||||||
"version": "0.5.1",
|
"version": "0.5.5",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"minimist": "0.0.8"
|
"minimist": "^1.2.5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"minimist": {
|
||||||
|
"version": "1.2.5",
|
||||||
|
"bundled": true,
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"move-concurrently": {
|
"move-concurrently": {
|
||||||
@@ -4651,7 +4659,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node-gyp": {
|
"node-gyp": {
|
||||||
"version": "5.0.7",
|
"version": "5.1.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -4669,7 +4677,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nopt": {
|
"nopt": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.3",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -4765,12 +4773,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npm-packlist": {
|
"npm-packlist": {
|
||||||
"version": "1.4.7",
|
"version": "1.4.8",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"ignore-walk": "^3.0.1",
|
"ignore-walk": "^3.0.1",
|
||||||
"npm-bundled": "^1.0.1"
|
"npm-bundled": "^1.0.1",
|
||||||
|
"npm-normalize-package-bin": "^1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npm-pick-manifest": {
|
"npm-pick-manifest": {
|
||||||
@@ -4784,7 +4793,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npm-profile": {
|
"npm-profile": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.4",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -4794,7 +4803,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"npm-registry-fetch": {
|
"npm-registry-fetch": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.5",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -4808,7 +4817,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": {
|
"safe-buffer": {
|
||||||
"version": "5.2.0",
|
"version": "5.2.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
@@ -5229,18 +5238,18 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"rc": {
|
"rc": {
|
||||||
"version": "1.2.7",
|
"version": "1.2.8",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"deep-extend": "^0.5.1",
|
"deep-extend": "^0.6.0",
|
||||||
"ini": "~1.3.0",
|
"ini": "~1.3.0",
|
||||||
"minimist": "^1.2.0",
|
"minimist": "^1.2.0",
|
||||||
"strip-json-comments": "~2.0.1"
|
"strip-json-comments": "~2.0.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimist": {
|
"minimist": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.5",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true
|
||||||
}
|
}
|
||||||
@@ -5299,7 +5308,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"readable-stream": {
|
"readable-stream": {
|
||||||
"version": "3.4.0",
|
"version": "3.6.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -5320,7 +5329,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"registry-auth-token": {
|
"registry-auth-token": {
|
||||||
"version": "3.3.2",
|
"version": "3.4.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -5384,7 +5393,7 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"rimraf": {
|
"rimraf": {
|
||||||
"version": "2.6.3",
|
"version": "2.7.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -5568,7 +5577,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"spdx-license-ids": {
|
"spdx-license-ids": {
|
||||||
"version": "3.0.3",
|
"version": "3.0.5",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
@@ -5683,11 +5692,18 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"string_decoder": {
|
"string_decoder": {
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"safe-buffer": {
|
||||||
|
"version": "5.2.0",
|
||||||
|
"bundled": true,
|
||||||
|
"dev": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stringify-package": {
|
"stringify-package": {
|
||||||
@@ -5996,7 +6012,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"widest-line": {
|
"widest-line": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"bundled": true,
|
"bundled": true,
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
"version": "0.0.0-managed-by-semantic-release",
|
"version": "0.0.0-managed-by-semantic-release",
|
||||||
"description": "Retries a GitHub Action step on failure or timeout.",
|
"description": "Retries a GitHub Action step on failure or timeout.",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"local": "node -r dotenv/config ./src/index.js",
|
||||||
"prepare": "ncc build src/index.js"
|
"prepare": "ncc build src/index.js"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"@semantic-release/changelog": "^3.0.6",
|
"@semantic-release/changelog": "^3.0.6",
|
||||||
"@semantic-release/git": "^7.0.18",
|
"@semantic-release/git": "^7.0.18",
|
||||||
"@zeit/ncc": "^0.20.5",
|
"@zeit/ncc": "^0.20.5",
|
||||||
|
"dotenv": "8.2.0",
|
||||||
"husky": "^3.1.0",
|
"husky": "^3.1.0",
|
||||||
"semantic-release": "^17.0.3"
|
"semantic-release": "^17.0.3"
|
||||||
},
|
},
|
||||||
|
|||||||
6
sample.env
Normal file
6
sample.env
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
INPUT_TIMEOUT_MINUTES=1
|
||||||
|
INPUT_MAX_ATTEMPTS=3
|
||||||
|
INPUT_COMMAND="node -e 'process.exit(99)'"
|
||||||
|
INPUT_RETRY_WAIT_SECONDS=10
|
||||||
|
INPUT_POLLING_INTERVAL_SECONDS=1
|
||||||
|
INPUT_RETRY_ON=any
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
const { execSync } = require('child_process');
|
const { exec } = require('child_process');
|
||||||
const COMMAND = process.argv.splice(2)[0];
|
const COMMAND = process.argv.splice(2)[0];
|
||||||
|
|
||||||
function run() {
|
function run() {
|
||||||
execSync(COMMAND, { stdio: 'inherit' });
|
exec(COMMAND, { stdio: 'inherit' }, (err) => {
|
||||||
|
if (err) {
|
||||||
|
process.exit(err.code);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
run();
|
run();
|
||||||
|
|||||||
113
src/index.js
113
src/index.js
@@ -1,13 +1,34 @@
|
|||||||
const { getInput, error, warning, info } = require('@actions/core');
|
const { getInput, error, warning, info, debug, setOutput } = require('@actions/core');
|
||||||
const { spawn } = require('child_process');
|
const { spawn } = require('child_process');
|
||||||
const { join } = require('path');
|
const { join } = require('path');
|
||||||
const ms = require('milliseconds');
|
const ms = require('milliseconds');
|
||||||
var kill = require('tree-kill');
|
var kill = require('tree-kill');
|
||||||
|
|
||||||
|
// inputs
|
||||||
|
const TIMEOUT_MINUTES = getInputNumber('timeout_minutes', false);
|
||||||
|
const TIMEOUT_SECONDS = getInputNumber('timeout_seconds', false);
|
||||||
|
const MAX_ATTEMPTS = getInputNumber('max_attempts', true);
|
||||||
|
const COMMAND = getInput('command', { required: true });
|
||||||
|
const RETRY_WAIT_SECONDS = getInputNumber('retry_wait_seconds', false);
|
||||||
|
const POLLING_INTERVAL_SECONDS = getInputNumber('polling_interval_seconds', false);
|
||||||
|
const RETRY_ON = getInput('retry_on') || 'any';
|
||||||
|
|
||||||
|
const OUTPUT_TOTAL_ATTEMPTS_KEY = 'total_attempts';
|
||||||
|
const OUTPUT_EXIT_CODE_KEY = 'exit_code';
|
||||||
|
const OUTPUT_EXIT_ERROR_KEY = 'exit_error';
|
||||||
|
|
||||||
|
var exit;
|
||||||
|
var done;
|
||||||
|
|
||||||
function getInputNumber(id, required) {
|
function getInputNumber(id, required) {
|
||||||
const input = getInput(id, { required });
|
const input = getInput(id, { required });
|
||||||
const num = Number.parseInt(input);
|
const num = Number.parseInt(input);
|
||||||
|
|
||||||
|
// empty is ok
|
||||||
|
if (!input && !required) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!Number.isInteger(num)) {
|
if (!Number.isInteger(num)) {
|
||||||
throw `Input ${id} only accepts numbers. Received ${input}`;
|
throw `Input ${id} only accepts numbers. Received ${input}`;
|
||||||
}
|
}
|
||||||
@@ -15,62 +36,112 @@ function getInputNumber(id, required) {
|
|||||||
return num;
|
return num;
|
||||||
}
|
}
|
||||||
|
|
||||||
// inputs
|
|
||||||
const TIMEOUT_MINUTES = getInputNumber('timeout_minutes', true);
|
|
||||||
const MAX_ATTEMPTS = getInputNumber('max_attempts', true);
|
|
||||||
const COMMAND = getInput('command', { required: true });
|
|
||||||
const RETRY_WAIT_SECONDS = getInputNumber('retry_wait_seconds', false);
|
|
||||||
const POLLING_INTERVAL_SECONDS = getInputNumber('polling_interval_seconds', false);
|
|
||||||
|
|
||||||
async function wait(ms) {
|
async function wait(ms) {
|
||||||
return new Promise(r => setTimeout(r, ms));
|
return new Promise((r) => setTimeout(r, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function retryWait() {
|
||||||
|
const waitStart = Date.now();
|
||||||
|
await wait(ms.seconds(RETRY_WAIT_SECONDS));
|
||||||
|
debug(`Waited ${Date.now() - waitStart}ms`);
|
||||||
|
debug(`Configured wait: ${ms.seconds(RETRY_WAIT_SECONDS)}ms`);
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
if (TIMEOUT_MINUTES) {
|
||||||
|
return ms.minutes(TIMEOUT_MINUTES);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ms.seconds(TIMEOUT_SECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runCmd() {
|
async function runCmd() {
|
||||||
const end_time = Date.now() + ms.minutes(TIMEOUT_MINUTES);
|
const end_time = Date.now() + getTimeout();
|
||||||
var done, exit;
|
|
||||||
|
exit = 0;
|
||||||
|
done = false;
|
||||||
|
|
||||||
var child = spawn('node', [join(__dirname, 'exec.js'), COMMAND], { stdio: 'inherit' });
|
var child = spawn('node', [join(__dirname, 'exec.js'), COMMAND], { stdio: 'inherit' });
|
||||||
|
|
||||||
child.on('exit', code => {
|
child.on('exit', (code, signal) => {
|
||||||
|
debug(`Code: ${code}`);
|
||||||
|
debug(`Signal: ${signal}`);
|
||||||
if (code > 0) {
|
if (code > 0) {
|
||||||
exit = code;
|
exit = code;
|
||||||
}
|
}
|
||||||
|
// timeouts are killed manually
|
||||||
|
if (signal === 'SIGTERM') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
done = true;
|
done = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
do {
|
do {
|
||||||
await wait(ms.seconds(POLLING_INTERVAL_SECONDS));
|
await wait(ms.seconds(POLLING_INTERVAL_SECONDS));
|
||||||
} while (Date.now() < end_time && !done && !exit);
|
} while (Date.now() < end_time && !done);
|
||||||
|
|
||||||
if (!done) {
|
if (!done) {
|
||||||
kill(child.pid);
|
kill(child.pid);
|
||||||
await wait(ms.seconds(RETRY_WAIT_SECONDS));
|
await retryWait();
|
||||||
throw new Error(`Timeout of ${TIMEOUT_MINUTES}m hit`);
|
throw new Error(`Timeout of ${getTimeout()}ms hit`);
|
||||||
} else if (exit > 0) {
|
} else if (exit > 0) {
|
||||||
throw new Error(`Child_process exited with error`);
|
await retryWait();
|
||||||
|
throw new Error(`Child_process exited with error code ${exit}`);
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runAction() {
|
async function runAction() {
|
||||||
|
await validateInputs();
|
||||||
|
|
||||||
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
||||||
try {
|
try {
|
||||||
|
// just keep overwriting attempts output
|
||||||
|
setOutput(OUTPUT_TOTAL_ATTEMPTS_KEY, attempt);
|
||||||
await runCmd();
|
await runCmd();
|
||||||
info(`Command completed after ${attempt} attempt(s).`);
|
info(`Command completed after ${attempt} attempt(s).`);
|
||||||
break;
|
break;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (attempt === MAX_ATTEMPTS) {
|
if (attempt === MAX_ATTEMPTS) {
|
||||||
throw new Error(`Final attempt failed. ${error.message}`);
|
throw new Error(`Final attempt failed. ${error.message}`);
|
||||||
|
} else if (!done && RETRY_ON === 'error') {
|
||||||
|
// error: timeout
|
||||||
|
throw error;
|
||||||
|
} else if (exit > 0 && RETRY_ON === 'timeout') {
|
||||||
|
// error: error
|
||||||
|
throw error;
|
||||||
} else {
|
} else {
|
||||||
warning(`Attempt ${attempt} failed. Reason:`, error.message);
|
warning(`Attempt ${attempt} failed. Reason: ${error.message}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
runAction().catch(err => {
|
runAction()
|
||||||
error(err.message);
|
.then(() => {
|
||||||
process.exit(1);
|
setOutput(OUTPUT_EXIT_CODE_KEY, 0);
|
||||||
});
|
process.exit(0); // success
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
error(err.message);
|
||||||
|
|
||||||
|
// these can be helpful to know if continue-on-error is true
|
||||||
|
setOutput(OUTPUT_EXIT_ERROR_KEY, err.message);
|
||||||
|
setOutput(OUTPUT_EXIT_CODE_KEY, exit > 0 ? exit : 1);
|
||||||
|
|
||||||
|
// exit with exact error code if available, otherwise just exit with 1
|
||||||
|
process.exit(exit > 0 ? exit : 1);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user