2022-08-05 23:31:37 -04:00
import { error , warning , info , debug , setOutput } from '@actions/core' ;
2022-08-03 23:02:05 -04:00
import { execSync , spawn } from 'child_process' ;
2020-11-14 11:45:32 -05:00
import ms from 'milliseconds' ;
import kill from 'tree-kill' ;
2022-08-05 23:31:37 -04:00
import { getInputs , getTimeout , Inputs , validateInputs } from './inputs' ;
import { retryWait , wait } from './util' ;
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' ;
2022-08-02 22:47:32 -04:00
let exit : number ;
let done : boolean ;
2020-09-29 10:48:02 -04:00
2022-08-05 23:31:37 -04:00
function getExecutable ( inputs : Inputs ) : string {
if ( ! inputs . shell ) {
2021-01-02 10:20:16 -05:00
return OS === 'win32' ? 'powershell' : 'bash' ;
}
2020-02-13 01:57:27 -05:00
2021-01-02 10:20:16 -05:00
let executable : string ;
2022-08-05 23:31:37 -04:00
switch ( inputs . shell ) {
2022-08-02 22:47:32 -04:00
case 'bash' :
case 'python' :
case 'pwsh' : {
2022-08-05 23:31:37 -04:00
executable = inputs . shell ;
2021-01-01 22:57:53 +00:00
break ;
}
2022-08-02 22:47:32 -04:00
case 'sh' : {
2021-01-02 10:20:16 -05:00
if ( OS === 'win32' ) {
2022-08-05 23:31:37 -04:00
throw new Error ( ` Shell ${ inputs . shell } not allowed on OS ${ OS } ` ) ;
2021-01-01 22:57:53 +00:00
}
2022-08-05 23:31:37 -04:00
executable = inputs . shell ;
2021-01-01 22:57:53 +00:00
break ;
}
2022-08-02 22:47:32 -04:00
case 'cmd' :
case 'powershell' : {
2021-01-02 10:20:16 -05:00
if ( OS !== 'win32' ) {
2022-08-05 23:31:37 -04:00
throw new Error ( ` Shell ${ inputs . shell } not allowed on OS ${ OS } ` ) ;
2021-01-01 22:57:53 +00:00
}
2022-08-05 23:31:37 -04:00
executable = inputs . shell + '.exe' ;
2021-01-01 22:57:53 +00:00
break ;
}
default : {
2022-08-02 22:47:32 -04:00
throw new Error (
2022-10-19 13:43:26 +03:00
` Shell ${ inputs . shell } not supported. See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell for supported shells `
2022-08-02 22:47:32 -04:00
) ;
2021-01-01 22:57:53 +00:00
}
}
2022-08-02 22:47:32 -04:00
return executable ;
2021-01-02 10:20:16 -05:00
}
2022-08-05 23:31:37 -04:00
async function runRetryCmd ( inputs : Inputs ) : Promise < void > {
2021-01-04 21:32:32 -05:00
// if no retry script, just continue
2022-08-05 23:31:37 -04:00
if ( ! inputs . on_retry_command ) {
2021-01-04 21:32:32 -05:00
return ;
}
try {
2022-08-05 23:31:37 -04:00
await execSync ( inputs . on_retry_command , { stdio : 'inherit' } ) ;
2022-08-02 22:47:32 -04:00
// eslint-disable-next-line
2022-08-02 21:55:30 -04:00
} catch ( error : any ) {
2022-08-02 22:47:32 -04:00
info ( ` WARNING: Retry command threw the error ${ error . message } ` ) ;
2021-01-04 21:32:32 -05:00
}
}
2022-08-05 23:31:37 -04:00
async function runCmd ( attempt : number , inputs : Inputs ) {
const end_time = Date . now ( ) + getTimeout ( inputs ) ;
const executable = getExecutable ( inputs ) ;
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
2022-08-05 23:31:37 -04:00
debug ( ` Running command ${ inputs . command } on ${ OS } using shell ${ executable } ` ) ;
2022-08-02 22:47:32 -04:00
const child =
2022-08-05 23:31:37 -04:00
attempt > 1 && inputs . new_command_on_retry
? spawn ( inputs . new_command_on_retry , { shell : executable } )
: spawn ( inputs . 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 {
2022-08-05 23:31:37 -04:00
await wait ( ms . seconds ( inputs . 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
2022-08-02 21:55:30 -04:00
if ( ! done && child . pid ) {
2020-02-13 01:57:27 -05:00
kill ( child . pid ) ;
2022-08-05 23:31:37 -04:00
await retryWait ( ms . seconds ( inputs . retry_wait_seconds ) ) ;
throw new Error ( ` Timeout of ${ getTimeout ( inputs ) } ms hit ` ) ;
2020-02-13 01:57:27 -05:00
} else if ( exit > 0 ) {
2022-08-05 23:31:37 -04:00
await retryWait ( ms . seconds ( inputs . retry_wait_seconds ) ) ;
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 ;
}
}
2022-08-05 23:31:37 -04:00
async function runAction ( inputs : Inputs ) {
await validateInputs ( inputs ) ;
2020-09-29 11:05:03 -04:00
2022-08-05 23:31:37 -04:00
for ( let attempt = 1 ; attempt <= inputs . max_attempts ; attempt ++ ) {
2020-02-13 01:57:27 -05:00
try {
2020-09-29 10:48:02 -04:00
// just keep overwriting attempts output
setOutput ( OUTPUT_TOTAL_ATTEMPTS_KEY , attempt ) ;
2022-08-05 23:31:37 -04:00
await runCmd ( attempt , inputs ) ;
2020-02-13 01:57:27 -05:00
info ( ` Command completed after ${ attempt } attempt(s). ` ) ;
break ;
2022-08-02 22:47:32 -04:00
// eslint-disable-next-line
2022-08-02 21:55:30 -04:00
} catch ( error : any ) {
2022-08-05 23:31:37 -04:00
if ( attempt === inputs . max_attempts ) {
2020-02-13 01:57:27 -05:00
throw new Error ( ` Final attempt failed. ${ error . message } ` ) ;
2022-08-05 23:31:37 -04:00
} else if ( ! done && inputs . retry_on === 'error' ) {
2020-09-21 20:47:01 +02:00
// error: timeout
throw error ;
2022-08-05 23:31:37 -04:00
} else if ( inputs . retry_on_exit_code && inputs . retry_on_exit_code !== exit ) {
2022-04-25 22:10:55 -04:00
throw error ;
2022-08-05 23:31:37 -04:00
} else if ( exit > 0 && inputs . retry_on === 'timeout' ) {
2020-09-29 14:56:52 -04:00
// error: error
2020-09-21 20:47:01 +02:00
throw error ;
2020-02-13 01:57:27 -05:00
} else {
2022-08-05 23:31:37 -04:00
await runRetryCmd ( inputs ) ;
if ( inputs . warning_on_retry ) {
2020-11-18 10:25:11 -05:00
warning ( ` Attempt ${ attempt } failed. Reason: ${ error . message } ` ) ;
} else {
info ( ` Attempt ${ attempt } failed. Reason: ${ error . message } ` ) ;
}
2020-02-13 01:57:27 -05:00
}
}
}
}
2022-08-05 23:31:37 -04:00
const inputs = getInputs ( ) ;
runAction ( inputs )
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 ;
2022-08-05 23:31:37 -04:00
if ( inputs . continue_on_error ) {
2021-06-10 18:08:08 -04:00
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
2022-08-05 23:31:37 -04:00
process . exit ( inputs . continue_on_error ? 0 : exitCode ) ;
2020-09-29 10:48:02 -04:00
} ) ;