2019-10-10 00:52:42 -04:00
using System ;
using System.IO ;
2022-02-11 16:18:41 -05:00
using System.Linq ;
2019-10-10 00:52:42 -04:00
using System.Text ;
using System.Threading.Tasks ;
using GitHub.DistributedTask.Pipelines.ContextData ;
2022-02-11 16:18:41 -05:00
using GitHub.DistributedTask.WebApi ;
2019-10-10 00:52:42 -04:00
using GitHub.Runner.Common ;
2022-02-14 15:06:08 +01:00
using GitHub.Runner.Common.Util ;
2019-10-10 00:52:42 -04:00
using GitHub.Runner.Sdk ;
2022-06-10 15:51:20 +02:00
using GitHub.Runner.Worker.Container ;
using GitHub.Runner.Worker.Container.ContainerHooks ;
2019-10-10 00:52:42 -04:00
using Pipelines = GitHub . DistributedTask . Pipelines ;
namespace GitHub.Runner.Worker.Handlers
{
[ServiceLocator(Default = typeof(ScriptHandler))]
public interface IScriptHandler : IHandler
{
ScriptActionExecutionData Data { get ; set ; }
}
public sealed class ScriptHandler : Handler , IScriptHandler
{
public ScriptActionExecutionData Data { get ; set ; }
2022-02-16 12:18:21 -05:00
protected override void PrintActionDetails ( ActionRunStage stage )
2019-10-10 00:52:42 -04:00
{
2022-03-18 02:35:04 +01:00
// if we're executing a Job Extension, we won't have an 'Action'
if ( ! IsActionStep )
2019-10-10 00:52:42 -04:00
{
2022-03-18 02:35:04 +01:00
if ( Inputs . TryGetValue ( "path" , out var path ) )
{
ExecutionContext . Output ( $"##[group]Run '{path}'" ) ;
}
else
{
throw new InvalidOperationException ( "Inputs 'path' must be set for job extensions" ) ;
}
2019-10-10 00:52:42 -04:00
}
2022-03-18 02:35:04 +01:00
else if ( Action . Type = = Pipelines . ActionSourceType . Script )
2019-10-10 00:52:42 -04:00
{
2022-03-18 02:35:04 +01:00
Inputs . TryGetValue ( "script" , out string contents ) ;
contents = contents ? ? string . Empty ;
2019-10-10 00:52:42 -04:00
var firstLine = contents . TrimStart ( ' ' , '\t' , '\r' , '\n' ) ;
var firstNewLine = firstLine . IndexOfAny ( new [ ] { '\r' , '\n' } ) ;
if ( firstNewLine > = 0 )
{
firstLine = firstLine . Substring ( 0 , firstNewLine ) ;
}
2021-09-01 16:04:27 -04:00
ExecutionContext . Output ( $"##[group]Run {firstLine}" ) ;
2022-03-18 02:35:04 +01:00
var multiLines = contents . Replace ( "\r\n" , "\n" ) . TrimEnd ( '\n' ) . Split ( '\n' ) ;
foreach ( var line in multiLines )
{
// Bright Cyan color
ExecutionContext . Output ( $"\x1b[36;1m{line}\x1b[0m" ) ;
}
2019-10-10 00:52:42 -04:00
}
else
{
2022-03-18 02:35:04 +01:00
throw new InvalidOperationException ( $"Invalid action type {Action?.Type} for {nameof(ScriptHandler)}" ) ;
2019-10-10 00:52:42 -04:00
}
string argFormat ;
string shellCommand ;
string shellCommandPath = null ;
bool validateShellOnHost = ! ( StepHost is ContainerStepHost ) ;
2020-07-19 19:05:47 -04:00
string prependPath = string . Join ( Path . PathSeparator . ToString ( ) , ExecutionContext . Global . PrependPath . Reverse < string > ( ) ) ;
2020-03-17 23:40:37 -04:00
string shell = null ;
if ( ! Inputs . TryGetValue ( "shell" , out shell ) | | string . IsNullOrEmpty ( shell ) )
{
// TODO: figure out how defaults interact with template later
// for now, we won't check job.defaults if we are inside a template.
2020-07-19 19:05:47 -04:00
if ( string . IsNullOrEmpty ( ExecutionContext . ScopeName ) & & ExecutionContext . Global . JobDefaults . TryGetValue ( "run" , out var runDefaults ) )
2020-03-17 23:40:37 -04:00
{
runDefaults . TryGetValue ( "shell" , out shell ) ;
}
}
2019-10-10 00:52:42 -04:00
if ( string . IsNullOrEmpty ( shell ) )
{
#if OS_WINDOWS
2019-10-23 11:24:02 -04:00
shellCommand = "pwsh" ;
2020-03-17 23:40:37 -04:00
if ( validateShellOnHost )
2019-10-10 00:52:42 -04:00
{
2023-09-26 16:49:15 -04:00
if ( ExecutionContext . Global . Variables . GetBoolean ( "DistributedTask.UseWhich2" ) = = true )
{
shellCommandPath = WhichUtil . Which2 ( shellCommand , require : false , Trace , prependPath ) ;
}
else
{
shellCommandPath = WhichUtil . Which ( shellCommand , require : false , Trace , prependPath ) ;
}
2019-10-23 11:24:02 -04:00
if ( string . IsNullOrEmpty ( shellCommandPath ) )
{
shellCommand = "powershell" ;
Trace . Info ( $"Defaulting to {shellCommand}" ) ;
2023-09-26 16:49:15 -04:00
if ( ExecutionContext . Global . Variables . GetBoolean ( "DistributedTask.UseWhich2" ) = = true )
{
shellCommandPath = WhichUtil . Which2 ( shellCommand , require : true , Trace , prependPath ) ;
}
else
{
shellCommandPath = WhichUtil . Which ( shellCommand , require : true , Trace , prependPath ) ;
}
2019-10-23 11:24:02 -04:00
}
2019-10-10 00:52:42 -04:00
}
#else
shellCommand = "sh" ;
if ( validateShellOnHost )
{
2023-09-26 16:49:15 -04:00
if ( ExecutionContext . Global . Variables . GetBoolean ( "DistributedTask.UseWhich2" ) = = true )
{
shellCommandPath = WhichUtil . Which2 ( "bash" , false , Trace , prependPath ) ? ? WhichUtil . Which2 ( "sh" , true , Trace , prependPath ) ;
}
else
{
shellCommandPath = WhichUtil . Which ( "bash" , false , Trace , prependPath ) ? ? WhichUtil . Which ( "sh" , true , Trace , prependPath ) ;
}
2019-10-10 00:52:42 -04:00
}
#endif
argFormat = ScriptHandlerHelpers . GetScriptArgumentsFormat ( shellCommand ) ;
}
else
{
var parsed = ScriptHandlerHelpers . ParseShellOptionString ( shell ) ;
shellCommand = parsed . shellCommand ;
if ( validateShellOnHost )
{
2023-09-26 16:49:15 -04:00
if ( ExecutionContext . Global . Variables . GetBoolean ( "DistributedTask.UseWhich2" ) = = true )
{
shellCommandPath = WhichUtil . Which2 ( parsed . shellCommand , true , Trace , prependPath ) ;
}
else
{
shellCommandPath = WhichUtil . Which ( parsed . shellCommand , true , Trace , prependPath ) ;
}
2019-10-10 00:52:42 -04:00
}
argFormat = $"{parsed.shellArgs}" . TrimStart ( ) ;
if ( string . IsNullOrEmpty ( argFormat ) )
{
argFormat = ScriptHandlerHelpers . GetScriptArgumentsFormat ( shellCommand ) ;
}
}
if ( ! string . IsNullOrEmpty ( shellCommandPath ) )
{
2021-09-01 16:04:27 -04:00
ExecutionContext . Output ( $"shell: {shellCommandPath} {argFormat}" ) ;
2019-10-10 00:52:42 -04:00
}
else
{
2021-09-01 16:04:27 -04:00
ExecutionContext . Output ( $"shell: {shellCommand} {argFormat}" ) ;
2019-10-10 00:52:42 -04:00
}
if ( this . Environment ? . Count > 0 )
{
2021-09-01 16:04:27 -04:00
ExecutionContext . Output ( "env:" ) ;
2019-10-10 00:52:42 -04:00
foreach ( var env in this . Environment )
{
2021-09-01 16:04:27 -04:00
ExecutionContext . Output ( $" {env.Key}: {env.Value}" ) ;
2019-10-10 00:52:42 -04:00
}
}
2021-09-01 16:04:27 -04:00
ExecutionContext . Output ( "##[endgroup]" ) ;
2019-10-10 00:52:42 -04:00
}
public async Task RunAsync ( ActionRunStage stage )
{
// Validate args
Trace . Entering ( ) ;
ArgUtil . NotNull ( ExecutionContext , nameof ( ExecutionContext ) ) ;
ArgUtil . NotNull ( Inputs , nameof ( Inputs ) ) ;
var githubContext = ExecutionContext . ExpressionValues [ "github" ] as GitHubContext ;
ArgUtil . NotNull ( githubContext , nameof ( githubContext ) ) ;
var tempDirectory = HostContext . GetDirectory ( WellKnownDirectory . Temp ) ;
Inputs . TryGetValue ( "script" , out var contents ) ;
contents = contents ? ? string . Empty ;
2020-03-17 23:40:37 -04:00
string workingDirectory = null ;
if ( ! Inputs . TryGetValue ( "workingDirectory" , out workingDirectory ) )
{
2022-04-06 08:33:32 -04:00
// Don't use job level working directories for hooks
if ( IsActionStep & & string . IsNullOrEmpty ( ExecutionContext . ScopeName ) & & ExecutionContext . Global . JobDefaults . TryGetValue ( "run" , out var runDefaults ) )
2020-03-17 23:40:37 -04:00
{
if ( runDefaults . TryGetValue ( "working-directory" , out workingDirectory ) )
{
ExecutionContext . Debug ( "Overwrite 'working-directory' base on job defaults." ) ;
}
}
}
2019-10-10 00:52:42 -04:00
var workspaceDir = githubContext [ "workspace" ] as StringContextData ;
workingDirectory = Path . Combine ( workspaceDir , workingDirectory ? ? string . Empty ) ;
2020-03-17 23:40:37 -04:00
string shell = null ;
if ( ! Inputs . TryGetValue ( "shell" , out shell ) | | string . IsNullOrEmpty ( shell ) )
{
2020-07-19 19:05:47 -04:00
if ( string . IsNullOrEmpty ( ExecutionContext . ScopeName ) & & ExecutionContext . Global . JobDefaults . TryGetValue ( "run" , out var runDefaults ) )
2020-03-17 23:40:37 -04:00
{
if ( runDefaults . TryGetValue ( "shell" , out shell ) )
{
ExecutionContext . Debug ( "Overwrite 'shell' base on job defaults." ) ;
}
}
}
2019-10-10 00:52:42 -04:00
var isContainerStepHost = StepHost is ContainerStepHost ;
2020-07-19 19:05:47 -04:00
string prependPath = string . Join ( Path . PathSeparator . ToString ( ) , ExecutionContext . Global . PrependPath . Reverse < string > ( ) ) ;
2019-10-10 00:52:42 -04:00
string commandPath , argFormat , shellCommand ;
// Set up default command and arguments
if ( string . IsNullOrEmpty ( shell ) )
{
#if OS_WINDOWS
2019-10-23 11:24:02 -04:00
shellCommand = "pwsh" ;
2023-09-26 16:49:15 -04:00
if ( ExecutionContext . Global . Variables . GetBoolean ( "DistributedTask.UseWhich2" ) = = true )
{
commandPath = WhichUtil . Which2 ( shellCommand , require : false , Trace , prependPath ) ;
}
else
{
commandPath = WhichUtil . Which ( shellCommand , require : false , Trace , prependPath ) ;
}
2019-10-23 11:24:02 -04:00
if ( string . IsNullOrEmpty ( commandPath ) )
{
shellCommand = "powershell" ;
Trace . Info ( $"Defaulting to {shellCommand}" ) ;
2023-09-26 16:49:15 -04:00
if ( ExecutionContext . Global . Variables . GetBoolean ( "DistributedTask.UseWhich2" ) = = true )
{
commandPath = WhichUtil . Which2 ( shellCommand , require : true , Trace , prependPath ) ;
}
else
{
commandPath = WhichUtil . Which ( shellCommand , require : true , Trace , prependPath ) ;
}
2019-10-23 11:24:02 -04:00
}
2019-10-21 14:08:12 -04:00
ArgUtil . NotNullOrEmpty ( commandPath , "Default Shell" ) ;
2019-10-10 00:52:42 -04:00
#else
shellCommand = "sh" ;
2023-09-26 16:49:15 -04:00
if ( ExecutionContext . Global . Variables . GetBoolean ( "DistributedTask.UseWhich2" ) = = true )
{
commandPath = WhichUtil . Which2 ( "bash" , false , Trace , prependPath ) ? ? WhichUtil . Which2 ( "sh" , true , Trace , prependPath ) ;
}
else
{
commandPath = WhichUtil . Which ( "bash" , false , Trace , prependPath ) ? ? WhichUtil . Which ( "sh" , true , Trace , prependPath ) ;
}
2019-10-10 00:52:42 -04:00
#endif
argFormat = ScriptHandlerHelpers . GetScriptArgumentsFormat ( shellCommand ) ;
}
else
{
2022-06-08 13:54:23 +02:00
// For these shells, we want to use system binaries
var systemShells = new string [ ] { "bash" , "sh" , "powershell" , "pwsh" } ;
if ( ! IsActionStep & & systemShells . Contains ( shell ) )
2019-10-10 00:52:42 -04:00
{
2022-06-08 13:54:23 +02:00
shellCommand = shell ;
2023-09-26 16:49:15 -04:00
if ( ExecutionContext . Global . Variables . GetBoolean ( "DistributedTask.UseWhich2" ) = = true )
{
commandPath = WhichUtil . Which2 ( shell , ! isContainerStepHost , Trace , prependPath ) ;
}
else
{
commandPath = WhichUtil . Which ( shell , ! isContainerStepHost , Trace , prependPath ) ;
}
2022-06-08 13:54:23 +02:00
if ( shell = = "bash" )
{
argFormat = ScriptHandlerHelpers . GetScriptArgumentsFormat ( "sh" ) ;
}
else
{
argFormat = ScriptHandlerHelpers . GetScriptArgumentsFormat ( shell ) ;
}
}
else
{
var parsed = ScriptHandlerHelpers . ParseShellOptionString ( shell ) ;
shellCommand = parsed . shellCommand ;
// For non-ContainerStepHost, the command must be located on the host by Which
2023-09-26 16:49:15 -04:00
if ( ExecutionContext . Global . Variables . GetBoolean ( "DistributedTask.UseWhich2" ) = = true )
{
commandPath = WhichUtil . Which2 ( parsed . shellCommand , ! isContainerStepHost , Trace , prependPath ) ;
}
else
{
commandPath = WhichUtil . Which ( parsed . shellCommand , ! isContainerStepHost , Trace , prependPath ) ;
}
2022-06-08 13:54:23 +02:00
argFormat = $"{parsed.shellArgs}" . TrimStart ( ) ;
if ( string . IsNullOrEmpty ( argFormat ) )
{
argFormat = ScriptHandlerHelpers . GetScriptArgumentsFormat ( shellCommand ) ;
}
2019-10-10 00:52:42 -04:00
}
}
2022-03-18 02:35:04 +01:00
// Don't override runner telemetry here
if ( ! string . IsNullOrEmpty ( shellCommand ) & & IsActionStep )
2022-02-16 12:18:21 -05:00
{
ExecutionContext . StepTelemetry . Action = shellCommand ;
}
2019-10-10 00:52:42 -04:00
// No arg format was given, shell must be a built-in
if ( string . IsNullOrEmpty ( argFormat ) | | ! argFormat . Contains ( "{0}" ) )
{
throw new ArgumentException ( "Invalid shell option. Shell must be a valid built-in (bash, sh, cmd, powershell, pwsh) or a format string containing '{0}'" ) ;
}
2022-03-18 02:35:04 +01:00
string scriptFilePath , resolvedScriptPath ;
if ( IsActionStep )
{
// We do not not the full path until we know what shell is being used, so that we can determine the file extension
scriptFilePath = Path . Combine ( tempDirectory , $"{Guid.NewGuid()}{ScriptHandlerHelpers.GetScriptFileExtension(shellCommand)}" ) ;
2022-06-10 15:51:20 +02:00
resolvedScriptPath = StepHost . ResolvePathForStepHost ( ExecutionContext , scriptFilePath ) . Replace ( "\"" , "\\\"" ) ;
2022-03-18 02:35:04 +01:00
}
else
{
// JobExtensionRunners run a script file, we load that from the inputs here
if ( ! Inputs . ContainsKey ( "path" ) )
{
throw new ArgumentException ( "Expected 'path' input to be set" ) ;
}
scriptFilePath = Inputs [ "path" ] ;
ArgUtil . NotNullOrEmpty ( scriptFilePath , "path" ) ;
resolvedScriptPath = Inputs [ "path" ] . Replace ( "\"" , "\\\"" ) ;
}
2019-10-10 00:52:42 -04:00
// Format arg string with script path
var arguments = string . Format ( argFormat , resolvedScriptPath ) ;
// Fix up and write the script
contents = ScriptHandlerHelpers . FixUpScriptContents ( shellCommand , contents ) ;
#if OS_WINDOWS
// Normalize Windows line endings
contents = contents . Replace ( "\r\n" , "\n" ) . Replace ( "\n" , "\r\n" ) ;
2020-07-19 19:05:47 -04:00
var encoding = ExecutionContext . Global . Variables . Retain_Default_Encoding & & Console . InputEncoding . CodePage ! = 65001
2019-10-10 00:52:42 -04:00
? Console . InputEncoding
: new UTF8Encoding ( false ) ;
#else
// Don't add a BOM. It causes the script to fail on some operating systems (e.g. on Ubuntu 14).
var encoding = new UTF8Encoding ( false ) ;
2022-03-18 02:35:04 +01:00
#endif
if ( IsActionStep )
{
// Script is written to local path (ie host) but executed relative to the StepHost, which may be a container
File . WriteAllText ( scriptFilePath , contents , encoding ) ;
}
2019-10-10 00:52:42 -04:00
// Prepend PATH
AddPrependPathToEnvironment ( ) ;
// expose context to environment
foreach ( var context in ExecutionContext . ExpressionValues )
{
if ( context . Value is IEnvironmentContextData runtimeContext & & runtimeContext ! = null )
{
foreach ( var env in runtimeContext . GetRuntimeEnvironmentVariables ( ) )
{
Environment [ env . Key ] = env . Value ;
}
}
}
// dump out the command
var fileName = isContainerStepHost ? shellCommand : commandPath ;
2020-04-09 16:13:06 -04:00
#if OS_OSX
if ( Environment . ContainsKey ( "DYLD_INSERT_LIBRARIES" ) ) // We don't check `isContainerStepHost` because we don't support container on macOS
{
// launch `node macOSRunInvoker.js shell args` instead of `shell args` to avoid macOS SIP remove `DYLD_INSERT_LIBRARIES` when launch process
2022-02-25 20:59:02 +01:00
string node = Path . Combine ( HostContext . GetDirectory ( WellKnownDirectory . Externals ) , NodeUtil . GetInternalNodeVersion ( ) , "bin" , $"node{IOUtil.ExeExtension}" ) ;
2020-04-09 16:13:06 -04:00
string macOSRunInvoker = Path . Combine ( HostContext . GetDirectory ( WellKnownDirectory . Bin ) , "macos-run-invoker.js" ) ;
arguments = $"\" { macOSRunInvoker . Replace ( "\"" , "\\\"" ) } \ " \"{fileName.Replace(" \ "" , "\\\"" ) } \ " {arguments}" ;
2022-02-14 15:06:08 +01:00
fileName = node ;
2020-04-09 16:13:06 -04:00
}
#endif
2021-08-26 10:29:02 -07:00
var systemConnection = ExecutionContext . Global . Endpoints . Single ( x = > string . Equals ( x . Name , WellKnownServiceEndpointNames . SystemVssConnection , StringComparison . OrdinalIgnoreCase ) ) ;
if ( systemConnection . Data . TryGetValue ( "GenerateIdTokenUrl" , out var generateIdTokenUrl ) & & ! string . IsNullOrEmpty ( generateIdTokenUrl ) )
{
Environment [ "ACTIONS_ID_TOKEN_REQUEST_URL" ] = generateIdTokenUrl ;
Environment [ "ACTIONS_ID_TOKEN_REQUEST_TOKEN" ] = systemConnection . Authorization . Parameters [ EndpointAuthorizationParameters . AccessToken ] ;
}
2019-10-10 00:52:42 -04:00
ExecutionContext . Debug ( $"{fileName} {arguments}" ) ;
2022-06-10 15:51:20 +02:00
Inputs . TryGetValue ( "standardInInput" , out var standardInInput ) ;
2019-10-10 00:52:42 -04:00
using ( var stdoutManager = new OutputManager ( ExecutionContext , ActionCommandManager ) )
using ( var stderrManager = new OutputManager ( ExecutionContext , ActionCommandManager ) )
{
StepHost . OutputDataReceived + = stdoutManager . OnDataReceived ;
StepHost . ErrorDataReceived + = stderrManager . OnDataReceived ;
// Execute
2022-06-10 15:51:20 +02:00
int exitCode = await StepHost . ExecuteAsync ( ExecutionContext ,
workingDirectory : StepHost . ResolvePathForStepHost ( ExecutionContext , workingDirectory ) ,
2019-10-10 00:52:42 -04:00
fileName : fileName ,
arguments : arguments ,
environment : Environment ,
requireExitCodeZero : false ,
outputEncoding : null ,
killProcessOnCancel : false ,
2020-07-19 19:05:47 -04:00
inheritConsoleHandler : ! ExecutionContext . Global . Variables . Retain_Default_Encoding ,
2022-06-10 15:51:20 +02:00
standardInInput : standardInInput ,
2019-10-10 00:52:42 -04:00
cancellationToken : ExecutionContext . CancellationToken ) ;
// Error
if ( exitCode ! = 0 )
{
ExecutionContext . Error ( $"Process completed with exit code {exitCode}." ) ;
ExecutionContext . Result = TaskResult . Failed ;
}
}
}
}
}