minor: added tests and helper outputs for PR #15

This commit is contained in:
Nick Fields
2020-09-29 10:48:02 -04:00
parent d2b20569e3
commit ec785f59e1
4 changed files with 171 additions and 41 deletions

View File

@@ -18,13 +18,21 @@ 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: 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 +40,97 @@ 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: true
actual: ${{ steps.sad_path_wait_sec.outputs.total_attempts == '3' && steps.sad_path_wait_sec.outcome == 'failure' }}
- uses: nick-invision/assert-action@v1
with:
expected: 'Command failed: npm install'
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: true
actual: ${{ steps.sad_path_error.outputs.total_attempts == '2' && steps.sad_path_error.outcome == 'failure' }}
- name: sad-path (timeout) - name: sad-path (timeout)
id: sad_path_timeout
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: true
actual: ${{ steps.sad_path_timeout.outputs.total_attempts == '2' && steps.sad_path_timeout.outcome == 'failure' }}
- name: retry_on (timeout)
id: retry_on_timeout
uses: ./
continue-on-error: true
with:
timeout_minutes: 1
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: true
actual: ${{ steps.retry_on_timeout.outputs.total_attempts == '2' && steps.retry_on_timeout.outcome == 'failure' }}
- name: retry_on (timeout) fails early if nonzero 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: true
actual: ${{ steps.retry_on_timeout_fail.outputs.total_attempts == '1' && steps.retry_on_timeout_fail.outcome == 'failure' && steps.retry_on_timeout_fail.outputs.exit_code == '2' }}
- name: retry_on (nonzero)
id: retry_on_nonzero
uses: ./
continue-on-error: true
with:
timeout_minutes: 1
max_attempts: 2
retry_on: timeout
command: node -e "process.exit(2)"
- uses: nick-invision/assert-action@v1
with:
expected: true
actual: ${{ steps.retry_on_nonzero.outputs.total_attempts == '2' && steps.retry_on_nonzero.outcome == 'failure' && steps.retry_on_nonzero.outputs.exit_code == '2' }}
- name: retry_on (nonzero) fails early if timeout encountered
id: retry_on_nonzero_fail
uses: ./
continue-on-error: true
with:
timeout_minutes: 1
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: true
actual: ${{ steps.retry_on_nonzero_fail.outputs.total_attempts == '2' && steps.retry_on_nonzero_fail.outcome == 'failure' && steps.retry_on_nonzero_fail.outputs.exit_code == '2' }}
# runs on push to master only # runs on push to master only
cd: cd:

View File

@@ -19,6 +19,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, nonzero]
outputs:
total_attempts:
description: The final number of attempts made
exit_code:
description: The final exit code returned by the command
exit_error:
descritipn: The final error returned by the command
runs: runs:
using: 'node12' using: 'node12'
main: 'dist/index.js' main: 'dist/index.js'

57
dist/index.js vendored
View File

@@ -414,12 +414,27 @@ 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 { 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', 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);
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);
@@ -431,20 +446,15 @@ 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() { async function runCmd() {
const end_time = Date.now() + ms.minutes(TIMEOUT_MINUTES); const end_time = Date.now() + ms.minutes(TIMEOUT_MINUTES);
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' });
@@ -457,7 +467,7 @@ async function runCmd() {
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);
@@ -465,7 +475,7 @@ async function runCmd() {
throw new Error(`Timeout of ${TIMEOUT_MINUTES}m hit`); throw new Error(`Timeout of ${TIMEOUT_MINUTES}m hit`);
} else if (exit > 0) { } else if (exit > 0) {
await retryWait(); await retryWait();
throw new Error(`Child_process exited with error`); throw new Error(`Child_process exited with error code ${exit}`);
} else { } else {
return; return;
} }
@@ -481,12 +491,20 @@ async function retryWait() {
async function runAction() { async function runAction() {
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 == 'nonzero') {
// error: timeout
throw error;
} else if (exit > 0 && RETRY_ON == 'timeout') {
// error: nonzero
throw error;
} else { } else {
warning(`Attempt ${attempt} failed. Reason:`, error.message); warning(`Attempt ${attempt} failed. Reason:`, error.message);
} }
@@ -494,10 +512,21 @@ async function runAction() {
} }
} }
runAction().catch((err) => { runAction()
.then(() => {
setOutput(OUTPUT_EXIT_CODE_KEY, 0);
process.exit(0); // success
})
.catch((err) => {
error(err.message); error(err.message);
process.exit(1);
}); // 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);
});
/***/ }), /***/ }),

View File

@@ -1,9 +1,24 @@
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 { 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', 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);
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);
@@ -15,21 +30,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);
const RETRY_ON = getInput('retry_on') || 'both';
async function wait(ms) { async function wait(ms) {
return new Promise((r) => setTimeout(r, ms)); return new Promise((r) => setTimeout(r, ms));
} }
var exit;
var done;
async function runCmd() { async function runCmd() {
const end_time = Date.now() + ms.minutes(TIMEOUT_MINUTES); const end_time = Date.now() + ms.minutes(TIMEOUT_MINUTES);
@@ -71,6 +75,8 @@ async function retryWait() {
async function runAction() { async function runAction() {
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;
@@ -91,16 +97,17 @@ async function runAction() {
} }
runAction() runAction()
.then(() => { .then(() => {
setOutput(OUTPUT_EXIT_CODE_KEY, 0);
process.exit(0); // success process.exit(0); // success
}) })
.catch((err) => { .catch((err) => {
error(err.message); error(err.message);
if (exit > 0) {
// error: nonzero // these can be helpful to know if continue-on-error is true
process.exit(exit); // copy exit code setOutput(OUTPUT_EXIT_ERROR_KEY, err.message);
} else { setOutput(OUTPUT_EXIT_CODE_KEY, exit > 0 ? exit : 1);
// error: Final attempt failed or timeout
process.exit(1); // exit with exact error code if available, otherwise just exit with 1
} process.exit(exit > 0 ? exit : 1);
}); });