mirror of
https://github.com/nick-fields/retry.git
synced 2026-02-09 22:58:02 +00:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c36bd33fae | ||
|
|
ad6c447324 | ||
|
|
36c6f604ab | ||
|
|
31e0097983 | ||
|
|
7a4513731b | ||
|
|
0a47821646 | ||
|
|
193acc1924 | ||
|
|
8965a748e1 | ||
|
|
c803451cc1 | ||
|
|
3f5463b526 | ||
|
|
915303cda5 | ||
|
|
86ecaf34fa | ||
|
|
ec785f59e1 | ||
|
|
d2b20569e3 | ||
|
|
7841cadab1 | ||
|
|
87ec0a8a32 | ||
|
|
fc84966019 |
20
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: nick-invision
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is, **including the snippet from your workflow `yaml` showing your configuration and command being executed.**
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Logs**
|
||||||
|
Enable [debug logging](https://docs.github.com/en/free-pro-team@latest/actions/managing-workflow-runs/enabling-debug-logging#enabling-step-debug-logging) then attach the [raw logs](https://docs.github.com/en/free-pro-team@latest/actions/managing-workflow-runs/using-workflow-run-logs#downloading-logs) (specifically the raw output of this action).
|
||||||
3
.github/scripts/log-examples.js
vendored
Normal file
3
.github/scripts/log-examples.js
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
console.log('console.log test');
|
||||||
|
process.stdout.write('stdout test\n');
|
||||||
|
process.stderr.write('stderr test\n');
|
||||||
150
.github/workflows/ci_cd.yml
vendored
150
.github/workflows/ci_cd.yml
vendored
@@ -18,13 +18,27 @@ jobs:
|
|||||||
node-version: 12
|
node-version: 12
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- 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: log examples
|
||||||
|
uses: ./
|
||||||
|
with:
|
||||||
|
command: node ./.github/scripts/log-examples.js
|
||||||
|
timeout_minutes: 1
|
||||||
|
|
||||||
- name: sad-path (retry_wait_seconds)
|
- name: sad-path (retry_wait_seconds)
|
||||||
|
id: sad_path_wait_sec
|
||||||
uses: ./
|
uses: ./
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
@@ -32,20 +46,156 @@ jobs:
|
|||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
retry_wait_seconds: 15
|
retry_wait_seconds: 15
|
||||||
command: npm install this-isnt-a-real-package-name-zzz
|
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 +0,0 @@
|
|||||||
const { execSync } = require('child_process');
|
|
||||||
const COMMAND = process.argv.splice(2)[0];
|
|
||||||
|
|
||||||
function run() {
|
|
||||||
execSync(COMMAND, { stdio: 'inherit' });
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
||||||
159
dist/index.js
vendored
159
dist/index.js
vendored
@@ -414,16 +414,37 @@ module.exports = require("path");
|
|||||||
/***/ 676:
|
/***/ 676:
|
||||||
/***/ (function(__unusedmodule, __unusedexports, __webpack_require__) {
|
/***/ (function(__unusedmodule, __unusedexports, __webpack_require__) {
|
||||||
|
|
||||||
const { getInput, error, warning, info, debug } = __webpack_require__(470);
|
const { getInput, error, warning, info, debug, setOutput } = __webpack_require__(470);
|
||||||
const { spawn } = __webpack_require__(129);
|
const child_process = __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}`;
|
||||||
}
|
}
|
||||||
@@ -431,46 +452,10 @@ 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 runCmd() {
|
|
||||||
const end_time = Date.now() + ms.minutes(TIMEOUT_MINUTES);
|
|
||||||
var done, exit;
|
|
||||||
|
|
||||||
var child = spawn('node', [__webpack_require__.ab + "exec.js", COMMAND], { stdio: 'inherit' });
|
|
||||||
|
|
||||||
child.on('exit', (code) => {
|
|
||||||
if (code > 0) {
|
|
||||||
exit = code;
|
|
||||||
}
|
|
||||||
done = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
do {
|
|
||||||
await wait(ms.seconds(POLLING_INTERVAL_SECONDS));
|
|
||||||
} while (Date.now() < end_time && !done && !exit);
|
|
||||||
|
|
||||||
if (!done) {
|
|
||||||
kill(child.pid);
|
|
||||||
await retryWait();
|
|
||||||
throw new Error(`Timeout of ${TIMEOUT_MINUTES}m hit`);
|
|
||||||
} else if (exit > 0) {
|
|
||||||
await retryWait();
|
|
||||||
throw new Error(`Child_process exited with error`);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function retryWait() {
|
async function retryWait() {
|
||||||
const waitStart = Date.now();
|
const waitStart = Date.now();
|
||||||
await wait(ms.seconds(RETRY_WAIT_SECONDS));
|
await wait(ms.seconds(RETRY_WAIT_SECONDS));
|
||||||
@@ -478,26 +463,114 @@ async function retryWait() {
|
|||||||
debug(`Configured wait: ${ms.seconds(RETRY_WAIT_SECONDS)}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() {
|
||||||
|
const end_time = Date.now() + getTimeout();
|
||||||
|
|
||||||
|
exit = 0;
|
||||||
|
done = false;
|
||||||
|
|
||||||
|
const file = COMMAND.split(' ')[0];
|
||||||
|
const args = COMMAND.split(' ').slice(1);
|
||||||
|
|
||||||
|
var child = child_process.exec(COMMAND, { stdio: 'inherit' });
|
||||||
|
|
||||||
|
child.stdout.on('data', (data) => {
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
|
child.stderr.on('data', (data) => {
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('exit', (code, signal) => {
|
||||||
|
debug(`Code: ${code}`);
|
||||||
|
debug(`Signal: ${signal}`);
|
||||||
|
if (code > 0) {
|
||||||
|
exit = code;
|
||||||
|
}
|
||||||
|
// timeouts are killed manually
|
||||||
|
if (signal === 'SIGTERM') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
done = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
do {
|
||||||
|
await wait(ms.seconds(POLLING_INTERVAL_SECONDS));
|
||||||
|
} while (Date.now() < end_time && !done);
|
||||||
|
|
||||||
|
if (!done) {
|
||||||
|
kill(child.pid);
|
||||||
|
await retryWait();
|
||||||
|
throw new Error(`Timeout of ${getTimeout()}ms hit`);
|
||||||
|
} else if (exit > 0) {
|
||||||
|
await retryWait();
|
||||||
|
throw new Error(`Child_process exited with error code ${exit}`);
|
||||||
|
} else {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
/***/ }),
|
/***/ }),
|
||||||
|
|||||||
116
package-lock.json
generated
116
package-lock.json
generated
@@ -5,9 +5,9 @@
|
|||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": {
|
"@actions/core": {
|
||||||
"version": "1.2.4",
|
"version": "1.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@actions/core/-/core-1.2.6.tgz",
|
||||||
"integrity": "sha512-YJCEq8BE3CdN8+7HPZ/4DxJjk/OkZV2FFIf+DlZTC/4iBlzYCD5yjRR6eiOS5llO11zbRltIRuKAjMKaWTE6cg=="
|
"integrity": "sha512-ZQYitnqiyBc3D+k7LsgSBmMDVkOVidaagDG7j3fOym77jNunWRuYx7VSHa9GNfFZh+zh61xsCjRj4JxMZlDqTA=="
|
||||||
},
|
},
|
||||||
"@babel/code-frame": {
|
"@babel/code-frame": {
|
||||||
"version": "7.8.3",
|
"version": "7.8.3",
|
||||||
@@ -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": {
|
||||||
@@ -17,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://github.com/nick-invision/retry#readme",
|
"homepage": "https://github.com/nick-invision/retry#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@actions/core": "^1.2.4",
|
"@actions/core": "^1.2.6",
|
||||||
"milliseconds": "^1.0.3",
|
"milliseconds": "^1.0.3",
|
||||||
"tree-kill": "^1.2.2"
|
"tree-kill": "^1.2.2"
|
||||||
},
|
},
|
||||||
@@ -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 +0,0 @@
|
|||||||
const { execSync } = require('child_process');
|
|
||||||
const COMMAND = process.argv.splice(2)[0];
|
|
||||||
|
|
||||||
function run() {
|
|
||||||
execSync(COMMAND, { stdio: 'inherit' });
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
||||||
159
src/index.js
159
src/index.js
@@ -1,13 +1,34 @@
|
|||||||
const { getInput, error, warning, info, debug } = require('@actions/core');
|
const { getInput, error, warning, info, debug, setOutput } = require('@actions/core');
|
||||||
const { spawn } = require('child_process');
|
const child_process = 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,46 +36,10 @@ 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 runCmd() {
|
|
||||||
const end_time = Date.now() + ms.minutes(TIMEOUT_MINUTES);
|
|
||||||
var done, exit;
|
|
||||||
|
|
||||||
var child = spawn('node', [join(__dirname, 'exec.js'), COMMAND], { stdio: 'inherit' });
|
|
||||||
|
|
||||||
child.on('exit', (code) => {
|
|
||||||
if (code > 0) {
|
|
||||||
exit = code;
|
|
||||||
}
|
|
||||||
done = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
do {
|
|
||||||
await wait(ms.seconds(POLLING_INTERVAL_SECONDS));
|
|
||||||
} while (Date.now() < end_time && !done && !exit);
|
|
||||||
|
|
||||||
if (!done) {
|
|
||||||
kill(child.pid);
|
|
||||||
await retryWait();
|
|
||||||
throw new Error(`Timeout of ${TIMEOUT_MINUTES}m hit`);
|
|
||||||
} else if (exit > 0) {
|
|
||||||
await retryWait();
|
|
||||||
throw new Error(`Child_process exited with error`);
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function retryWait() {
|
async function retryWait() {
|
||||||
const waitStart = Date.now();
|
const waitStart = Date.now();
|
||||||
await wait(ms.seconds(RETRY_WAIT_SECONDS));
|
await wait(ms.seconds(RETRY_WAIT_SECONDS));
|
||||||
@@ -62,23 +47,111 @@ async function retryWait() {
|
|||||||
debug(`Configured wait: ${ms.seconds(RETRY_WAIT_SECONDS)}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() {
|
||||||
|
const end_time = Date.now() + getTimeout();
|
||||||
|
|
||||||
|
exit = 0;
|
||||||
|
done = false;
|
||||||
|
|
||||||
|
const file = COMMAND.split(' ')[0];
|
||||||
|
const args = COMMAND.split(' ').slice(1);
|
||||||
|
|
||||||
|
var child = child_process.exec(COMMAND, { stdio: 'inherit' });
|
||||||
|
|
||||||
|
child.stdout.on('data', (data) => {
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
|
child.stderr.on('data', (data) => {
|
||||||
|
console.log(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
child.on('exit', (code, signal) => {
|
||||||
|
debug(`Code: ${code}`);
|
||||||
|
debug(`Signal: ${signal}`);
|
||||||
|
if (code > 0) {
|
||||||
|
exit = code;
|
||||||
|
}
|
||||||
|
// timeouts are killed manually
|
||||||
|
if (signal === 'SIGTERM') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
done = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
do {
|
||||||
|
await wait(ms.seconds(POLLING_INTERVAL_SECONDS));
|
||||||
|
} while (Date.now() < end_time && !done);
|
||||||
|
|
||||||
|
if (!done) {
|
||||||
|
kill(child.pid);
|
||||||
|
await retryWait();
|
||||||
|
throw new Error(`Timeout of ${getTimeout()}ms hit`);
|
||||||
|
} else if (exit > 0) {
|
||||||
|
await retryWait();
|
||||||
|
throw new Error(`Child_process exited with error code ${exit}`);
|
||||||
|
} else {
|
||||||
|
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