2020-11-14 11:45:32 -05:00
import { getInput , error , warning , info , debug , setOutput } from '@actions/core' ;
2021-01-04 21:32:32 -05:00
import { exec , execSync } from 'child_process' ;
2020-11-14 11:45:32 -05:00
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' ;
2021-01-04 21:32:32 -05:00
const ON_RETRY_COMMAND = getInput ( 'on_retry_command' ) ;
2021-06-10 18:08:08 -04:00
const CONTINUE_ON_ERROR = getInputBoolean ( 'continue_on_error' ) ;
2021-12-08 20:15:21 -08:00
const NEW_COMMAND_ON_RETRY = getInput ( 'new_command_on_retry' ) ;
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 ;
}
2021-06-10 18:08:08 -04:00
function getInputBoolean ( id : string ) : Boolean {
const input = getInput ( id ) ;
if ( ! [ 'true' , 'false' ] . includes ( input . toLowerCase ( ) ) ) {
throw ` Input ${ id } only accepts boolean values. Received ${ input } ` ;
}
return input . toLowerCase ( ) === 'true'
}
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 : {
2021-01-03 21:00:31 -05:00
throw new Error ( ` Shell ${ SHELL } not supported. See https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell for supported shells ` ) ;
2021-01-01 22:57:53 +00:00
}
}
2021-01-02 10:20:16 -05:00
return executable
}
2021-01-04 21:32:32 -05:00
async function runRetryCmd ( ) : Promise < void > {
// if no retry script, just continue
if ( ! ON_RETRY_COMMAND ) {
return ;
}
try {
await execSync ( ON_RETRY_COMMAND , { stdio : 'inherit' } ) ;
} catch ( error ) {
info ( ` WARNING: Retry command threw the error ${ error . message } ` )
}
}
2021-12-08 20:15:21 -08:00
async function runCmd ( attempt : number ) {
2021-01-02 10:20:16 -05:00
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-12-08 20:15:21 -08:00
var child = attempt > 1 && NEW_COMMAND_ON_RETRY
? exec ( NEW_COMMAND_ON_RETRY , { 'shell' : executable } )
: 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 ) ;
2021-12-08 20:15:21 -08:00
await runCmd ( attempt ) ;
2020-02-13 01:57:27 -05:00
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 {
2021-01-04 21:32:32 -05:00
await runRetryCmd ( ) ;
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 ) = > {
2021-06-10 18:08:08 -04:00
// exact error code if available, otherwise just 1
const exitCode = exit > 0 ? exit : 1 ;
if ( CONTINUE_ON_ERROR ) {
warning ( err . message ) ;
} else {
error ( err . message ) ;
}
2020-09-29 10:48:02 -04:00
// these can be helpful to know if continue-on-error is true
setOutput ( OUTPUT_EXIT_ERROR_KEY , err . message ) ;
2021-06-10 18:08:08 -04:00
setOutput ( OUTPUT_EXIT_CODE_KEY , exitCode ) ;
2020-09-29 10:48:02 -04:00
2021-06-10 18:08:08 -04:00
// if continue_on_error, exit with exact error code else exit gracefully
// mimics native continue-on-error that is not supported in composite actions
process . exit ( CONTINUE_ON_ERROR ? 0 : exitCode ) ;
2020-09-29 10:48:02 -04:00
} ) ;