2020-11-14 11:45:32 -05:00
|
|
|
import { getInput, error, warning, info, debug, setOutput } from '@actions/core';
|
|
|
|
|
import { exec } from 'child_process';
|
|
|
|
|
import ms from 'milliseconds';
|
|
|
|
|
import kill from 'tree-kill';
|
|
|
|
|
|
|
|
|
|
import { wait } from './util';
|
2020-02-13 01:57:27 -05:00
|
|
|
|
2020-09-29 10:48:02 -04:00
|
|
|
// inputs
|
2020-09-29 11:05:03 -04:00
|
|
|
const TIMEOUT_MINUTES = getInputNumber('timeout_minutes', false);
|
|
|
|
|
const TIMEOUT_SECONDS = getInputNumber('timeout_seconds', false);
|
2020-11-14 11:45:32 -05:00
|
|
|
const MAX_ATTEMPTS = getInputNumber('max_attempts', true) || 3;
|
2020-09-29 10:48:02 -04:00
|
|
|
const COMMAND = getInput('command', { required: true });
|
2020-11-14 11:45:32 -05:00
|
|
|
const RETRY_WAIT_SECONDS = getInputNumber('retry_wait_seconds', false) || 10;
|
2021-01-02 10:20:16 -05:00
|
|
|
const SHELL = getInput('shell');
|
2020-11-14 11:45:32 -05:00
|
|
|
const POLLING_INTERVAL_SECONDS = getInputNumber('polling_interval_seconds', false) || 1;
|
2020-09-29 10:48:02 -04:00
|
|
|
const RETRY_ON = getInput('retry_on') || 'any';
|
2020-11-18 10:25:11 -05:00
|
|
|
const WARNING_ON_RETRY = getInput('warning_on_retry').toLowerCase() === 'true';
|
2020-09-29 10:48:02 -04:00
|
|
|
|
2021-01-02 10:20:16 -05:00
|
|
|
const OS = process.platform;
|
2020-09-29 10:48:02 -04:00
|
|
|
const OUTPUT_TOTAL_ATTEMPTS_KEY = 'total_attempts';
|
|
|
|
|
const OUTPUT_EXIT_CODE_KEY = 'exit_code';
|
|
|
|
|
const OUTPUT_EXIT_ERROR_KEY = 'exit_error';
|
|
|
|
|
|
2020-11-14 11:45:32 -05:00
|
|
|
var exit: number;
|
|
|
|
|
var done: boolean;
|
2020-09-29 10:48:02 -04:00
|
|
|
|
2020-11-14 11:45:32 -05:00
|
|
|
function getInputNumber(id: string, required: boolean): number | undefined {
|
2020-02-13 01:57:27 -05:00
|
|
|
const input = getInput(id, { required });
|
|
|
|
|
const num = Number.parseInt(input);
|
|
|
|
|
|
2020-09-29 11:05:03 -04:00
|
|
|
// empty is ok
|
|
|
|
|
if (!input && !required) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2020-02-13 01:57:27 -05:00
|
|
|
if (!Number.isInteger(num)) {
|
|
|
|
|
throw `Input ${id} only accepts numbers. Received ${input}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return num;
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-29 11:05:03 -04:00
|
|
|
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');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-11-14 11:45:32 -05:00
|
|
|
function getTimeout(): number {
|
2020-09-29 11:05:03 -04:00
|
|
|
if (TIMEOUT_MINUTES) {
|
|
|
|
|
return ms.minutes(TIMEOUT_MINUTES);
|
2020-11-14 11:45:32 -05:00
|
|
|
} else if (TIMEOUT_SECONDS) {
|
|
|
|
|
return ms.seconds(TIMEOUT_SECONDS);
|
2020-09-29 11:05:03 -04:00
|
|
|
}
|
|
|
|
|
|
2020-11-14 11:45:32 -05:00
|
|
|
throw new Error('Must specify either timeout_minutes or timeout_seconds inputs');
|
2020-09-29 11:05:03 -04:00
|
|
|
}
|
|
|
|
|
|
2021-01-02 10:20:16 -05:00
|
|
|
function getExecutable(): string {
|
|
|
|
|
if (!SHELL) {
|
|
|
|
|
return OS === 'win32' ? 'powershell' : 'bash';
|
|
|
|
|
}
|
2020-02-13 01:57:27 -05:00
|
|
|
|
2021-01-02 10:20:16 -05:00
|
|
|
let executable: string;
|
2021-01-01 22:57:53 +00:00
|
|
|
switch (SHELL) {
|
2021-01-02 10:20:16 -05:00
|
|
|
case "bash":
|
|
|
|
|
case "python":
|
2021-01-01 22:57:53 +00:00
|
|
|
case "pwsh": {
|
|
|
|
|
executable = SHELL;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
case "sh": {
|
2021-01-02 10:20:16 -05:00
|
|
|
if (OS === 'win32') {
|
2021-01-01 22:57:53 +00:00
|
|
|
throw new Error(`Shell ${SHELL} not allowed on OS ${OS}`);
|
|
|
|
|
}
|
|
|
|
|
executable = SHELL;
|
|
|
|
|
break;
|
|
|
|
|
}
|
2021-01-02 10:20:16 -05:00
|
|
|
case "cmd":
|
2021-01-01 22:57:53 +00:00
|
|
|
case "powershell": {
|
2021-01-02 10:20:16 -05:00
|
|
|
if (OS !== 'win32') {
|
2021-01-01 22:57:53 +00:00
|
|
|
throw new Error(`Shell ${SHELL} not allowed on OS ${OS}`);
|
|
|
|
|
}
|
|
|
|
|
executable = SHELL + ".exe";
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
default: {
|
|
|
|
|
throw new Error(`Shell ${SHELL} required`);
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-01-02 10:20:16 -05:00
|
|
|
return executable
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function runCmd() {
|
|
|
|
|
const end_time = Date.now() + getTimeout();
|
|
|
|
|
const executable = getExecutable();
|
2021-01-01 22:57:53 +00:00
|
|
|
|
2021-01-02 10:20:16 -05:00
|
|
|
exit = 0;
|
|
|
|
|
done = false;
|
2021-01-01 22:57:53 +00:00
|
|
|
|
2021-01-02 10:20:16 -05:00
|
|
|
debug(`Running command ${COMMAND} on ${OS} using shell ${executable}`)
|
2021-01-01 22:57:53 +00:00
|
|
|
var child = exec(COMMAND, { 'shell': executable });
|
2020-11-14 09:11:33 -05:00
|
|
|
|
2020-11-14 11:45:32 -05:00
|
|
|
child.stdout?.on('data', (data) => {
|
|
|
|
|
process.stdout.write(data);
|
2020-11-14 09:11:33 -05:00
|
|
|
});
|
2020-11-14 11:45:32 -05:00
|
|
|
child.stderr?.on('data', (data) => {
|
|
|
|
|
process.stdout.write(data);
|
2020-11-14 09:11:33 -05:00
|
|
|
});
|
2020-02-13 01:57:27 -05:00
|
|
|
|
2020-09-29 11:05:03 -04:00
|
|
|
child.on('exit', (code, signal) => {
|
|
|
|
|
debug(`Code: ${code}`);
|
|
|
|
|
debug(`Signal: ${signal}`);
|
2020-11-14 11:45:32 -05:00
|
|
|
if (code && code > 0) {
|
2020-02-13 01:57:27 -05:00
|
|
|
exit = code;
|
|
|
|
|
}
|
2020-09-29 11:05:03 -04:00
|
|
|
// timeouts are killed manually
|
|
|
|
|
if (signal === 'SIGTERM') {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-02-13 01:57:27 -05:00
|
|
|
done = true;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
do {
|
|
|
|
|
await wait(ms.seconds(POLLING_INTERVAL_SECONDS));
|
2020-09-21 20:47:01 +02:00
|
|
|
} while (Date.now() < end_time && !done);
|
2020-02-13 01:57:27 -05:00
|
|
|
|
|
|
|
|
if (!done) {
|
|
|
|
|
kill(child.pid);
|
2020-06-17 13:52:49 -04:00
|
|
|
await retryWait();
|
2020-09-29 11:05:03 -04:00
|
|
|
throw new Error(`Timeout of ${getTimeout()}ms hit`);
|
2020-02-13 01:57:27 -05:00
|
|
|
} else if (exit > 0) {
|
2020-06-17 13:52:49 -04:00
|
|
|
await retryWait();
|
2020-09-21 20:47:01 +02:00
|
|
|
throw new Error(`Child_process exited with error code ${exit}`);
|
2020-02-13 01:57:27 -05:00
|
|
|
} else {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function runAction() {
|
2020-09-29 11:05:03 -04:00
|
|
|
await validateInputs();
|
|
|
|
|
|
2020-02-13 01:57:27 -05:00
|
|
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
|
|
|
|
try {
|
2020-09-29 10:48:02 -04:00
|
|
|
// just keep overwriting attempts output
|
|
|
|
|
setOutput(OUTPUT_TOTAL_ATTEMPTS_KEY, attempt);
|
2020-02-13 01:57:27 -05:00
|
|
|
await runCmd();
|
|
|
|
|
info(`Command completed after ${attempt} attempt(s).`);
|
|
|
|
|
break;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (attempt === MAX_ATTEMPTS) {
|
|
|
|
|
throw new Error(`Final attempt failed. ${error.message}`);
|
2020-09-29 14:56:52 -04:00
|
|
|
} else if (!done && RETRY_ON === 'error') {
|
2020-09-21 20:47:01 +02:00
|
|
|
// error: timeout
|
|
|
|
|
throw error;
|
2020-09-29 14:56:52 -04:00
|
|
|
} else if (exit > 0 && RETRY_ON === 'timeout') {
|
|
|
|
|
// error: error
|
2020-09-21 20:47:01 +02:00
|
|
|
throw error;
|
2020-02-13 01:57:27 -05:00
|
|
|
} else {
|
2020-11-18 10:25:11 -05:00
|
|
|
if (WARNING_ON_RETRY) {
|
|
|
|
|
warning(`Attempt ${attempt} failed. Reason: ${error.message}`);
|
|
|
|
|
} else {
|
|
|
|
|
info(`Attempt ${attempt} failed. Reason: ${error.message}`);
|
|
|
|
|
}
|
2020-02-13 01:57:27 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-09-21 20:47:01 +02:00
|
|
|
runAction()
|
2020-09-29 10:48:02 -04:00
|
|
|
.then(() => {
|
|
|
|
|
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);
|
|
|
|
|
});
|