mirror of
https://github.com/actions/runner.git
synced 2025-12-10 12:21:58 +00:00
* Added ability to run Dockerfile.SUFFIX ContainerAction * Extracted IsDockerFile method * reformatted, moved from index to Last() * extracted IsDockerfile to DockerUtil with L0 * added check for IsDockerfile to account for docker:// * updated test to clearly show path/dockerfile:tag * fail if Data.Image is not Dockerfile or docker://[image] * Setup noops for JobPrepare and JobCleanup hooks * Add container jobstarted and jobcomplete hooks * Run 'index.js' instead of specific command hooks * Call jobprepare with command arg * Use right command name (hardcoded) Co-authored-by: Nikola Jokic <nikola-jokic@users.noreply.github.com> * Invoke hooks with arguments * Add PrepareJob hook to work with jobcontainers Co-authored-by: Nikola Jokic <nikola-jokic@users.noreply.github.com> * Rename methods * Use new hookcontainer to run prep and clean hooks * Get path from ENV * Use enums * Use IOUtils.cs * Move container files to folder * Move namespaces * Store "state" between hooks * Remove stdin stream in containerstephosts * Update Constants.cs * Throw if stdin fails * Cleanup obvious nullrefs and unused vars * Cleanup containerhook directory * Call step exec hook * Fix windows build * Remove hook from hookContainer * Rename file * More renamings * Add TODOs * Fix env name * Fix missing imports * Fix imports * Run script step on jobcontainer * Enable feature if env is set * Update ContainerHookManager.cs * Update ContainerHookManager.cs * Hooks allowed to work even when context isn't returned * Custom hooks enabled flag and additional null checks * New line at the end of the FeatureFlagManager.cs * Code refactoring * Supported just in time container building or pulling * Try mock-build for osx * Build all platforms * Run mock on self-hosted * Remove GITHUB prefix * Use ContainerHooksPath instead of CustomHooksPath * Null checks simplified * Code refactoring * Changing condition for image builing/pulling * Code refactoring * TODO comment removed Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com> * Call container step if FF is on * Rename run script function * Use JToken instead of dynamic * Add TODO * Small refactoring + renames + TODOs * Throw on DetermineNodeRuntimeVersion * Fix formatting * Add run-container-step * Supported nodeJS in Alpine containers * Renamed Alpine to IsAlpine in HookResponse * Method for checking platform for alpine container * Added container hooks feature flag check * Update IsHookFeatureEnabled with new params * Rename featureflag method * Finish rename * Set collection null values to empty arrays when JSON serialising them * Disable FF until we merge * Update src/Runner.Worker/Container/ContainerHooks/HookContainer.cs * Fix method name * Change hookargs to superclass from interface * Using only Path.Combine in GenerateResponsePath * fix merge error * EntryPointArgs changed to list of args instead of one args string * Changed List to IEnumerable for EntryPointArgs and MountVolumes * Get ContainerRuntimePath for JobContainers from hooks * Read ContainerEnv from response file * Port mappings saved after creating services * Support case when responseFile doesn't exist * Check if response file exists * Logging in ExecuteHookScript * Save hook state after all 4 hooks * Code refactoring * Remove TODO Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com> * Remove second TODO Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com> * Removing container env changes * Removing containerEnv and dockerManager * Delete mock-build.yml * Update IOUtil.cs * Add comment about containerhooks * Fix merge mistake * Remove solved todo * Determine which shell to use for hooks scenario * Overload for method ExecuteHookScript with prependPath as arg * Adding HostContext to the GetDefaultShellForScript call * prependPath as a mandatory parameter * Improve logging for hooks * Small changes in logging * Allow null for ContainerEntryPointArgs * Changed log messages * Skip setting EntryPoint and EntryPointArgs if hooks are enabled * Throw if IsAlpine is null in PrepareJob * Code refactoring - added GetAndValidateResponse method * Code refactoring * Changes in exception message * Only save hookState if returned * Use FF from server * Empty line * Code refactoring Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com> * Send null instead of string empty * Remove TODO * Code refactoring and some small changes * Allow Globals to be null to pass L0 * Fix setup in StepHostL0 * Throw exception earlier if response file doesn't exist and prepare_job hook is running * Refactoring GetResponse method * Changing exception message if response file is not found Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com> * Chaning exception message if isAlpine is null for prepare_job hook * Rename hook folder * Fail if compatible hookfile not found * Use .Value instead of casting bool? to bool * Format spacing * Formatting * User user and system mvs * Use variables instead of entire context in featuremanager * Update stepTelemetry if step uses containerhooks * Restore import * Remove unneccessary field from HookContainer * Refactor response context and portmappings * Force allow hooks if FF is on * Code refactoring * Revert deleting usings * Better hookContainer defaults and use correct portmapping list * Make GetDefaultShellForScript a HostContext extension method * Generic hookresponse * Code refactoring, unnecessary properties removed - HookContainer moved to the HookInput.cs * Remove empty line * Code refactoring and better exception handling * code refactor, removing unnecessary props * Move hookstate to global ContainerHookState * Trace exception before we throw it for not losing information * Fix for null ref exception in GetResponse * Adding additional check for null response in prepareJob hook * Refactoring GetResponse with additional check * Update error messages * Ports in ResponseContainer changed from IList to IDictionary * Fix port format * Include dockerfile * Send null Registry obj if there's nothing in it * Minor formatting * Check if hookIndexPath exists relocated to the ContainerHookManager * Code refactoring - ValidateHookExecutable added to the ContainerHookManager * check if ContainerHooksPath when AllowRunnerContainerHooks is on * Submit JSON telemetry instead of boolean * Prefix step hooks with "run" * Rename FeatureManager * Fix flipped condition * Unify js shell path getter with ps1 and sh getter * Validate on run, not on instantiation of manager * Cleanup ExecuteAsync methods * Handle exception in executeHookScript * Better exception types * Remove comment * Simplify boolean * Allow jobs without jobContainer to run * Use JObject instead of JToken * Use correct Response type * Format class to move cleanupJobAsync to the end of public methods * Rename HookIndexPath to HookScriptPath * Refactor methods into expression bodies * Fix args class hierarchy * Fix argument order * Formatting * Fix nullref and don't swallow stacktrace * Whilelist HookArgs * Use FF in FeatureManager * Update src/Runner.Worker/ContainerOperationProvider.cs Co-authored-by: Tingluo Huang <tingluohuang@github.com> * Update src/Runner.Worker/ActionRunner.cs Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com> * Update src/Runner.Worker/ActionRunner.cs Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com> * Only mount well known dirs to job containers * Get trace from hostcontext * Use hook execution for setting telemetry Co-authored-by: Nikola Jokic <nikola.jokic@akvelon.com> Co-authored-by: Nikola Jokic <nikola-jokic@users.noreply.github.com> Co-authored-by: Nikola Jokic <97525037+nikola-jokic@users.noreply.github.com> Co-authored-by: Stefan Ruvceski <stefan.ruvceski@akvelon.com> Co-authored-by: ruvceskistefan <96768603+ruvceskistefan@users.noreply.github.com> Co-authored-by: Thomas Boop <thboop@github.com> Co-authored-by: stefanruvceski <ruvceskistefan@github.com> Co-authored-by: Tingluo Huang <tingluohuang@github.com> Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>
353 lines
16 KiB
C#
353 lines
16 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using GitHub.DistributedTask.Pipelines.ContextData;
|
|
using GitHub.DistributedTask.WebApi;
|
|
using GitHub.Runner.Common;
|
|
using GitHub.Runner.Common.Util;
|
|
using GitHub.Runner.Sdk;
|
|
using GitHub.Runner.Worker.Container;
|
|
using GitHub.Runner.Worker.Container.ContainerHooks;
|
|
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; }
|
|
|
|
protected override void PrintActionDetails(ActionRunStage stage)
|
|
{
|
|
// if we're executing a Job Extension, we won't have an 'Action'
|
|
if (!IsActionStep)
|
|
{
|
|
if (Inputs.TryGetValue("path", out var path))
|
|
{
|
|
ExecutionContext.Output($"##[group]Run '{path}'");
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidOperationException("Inputs 'path' must be set for job extensions");
|
|
}
|
|
}
|
|
else if (Action.Type == Pipelines.ActionSourceType.Script)
|
|
{
|
|
Inputs.TryGetValue("script", out string contents);
|
|
contents = contents ?? string.Empty;
|
|
var firstLine = contents.TrimStart(' ', '\t', '\r', '\n');
|
|
var firstNewLine = firstLine.IndexOfAny(new[] { '\r', '\n' });
|
|
if (firstNewLine >= 0)
|
|
{
|
|
firstLine = firstLine.Substring(0, firstNewLine);
|
|
}
|
|
|
|
ExecutionContext.Output($"##[group]Run {firstLine}");
|
|
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");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
throw new InvalidOperationException($"Invalid action type {Action?.Type} for {nameof(ScriptHandler)}");
|
|
}
|
|
|
|
string argFormat;
|
|
string shellCommand;
|
|
string shellCommandPath = null;
|
|
bool validateShellOnHost = !(StepHost is ContainerStepHost);
|
|
string prependPath = string.Join(Path.PathSeparator.ToString(), ExecutionContext.Global.PrependPath.Reverse<string>());
|
|
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.
|
|
if (string.IsNullOrEmpty(ExecutionContext.ScopeName) && ExecutionContext.Global.JobDefaults.TryGetValue("run", out var runDefaults))
|
|
{
|
|
runDefaults.TryGetValue("shell", out shell);
|
|
}
|
|
}
|
|
if (string.IsNullOrEmpty(shell))
|
|
{
|
|
#if OS_WINDOWS
|
|
shellCommand = "pwsh";
|
|
if (validateShellOnHost)
|
|
{
|
|
shellCommandPath = WhichUtil.Which(shellCommand, require: false, Trace, prependPath);
|
|
if (string.IsNullOrEmpty(shellCommandPath))
|
|
{
|
|
shellCommand = "powershell";
|
|
Trace.Info($"Defaulting to {shellCommand}");
|
|
shellCommandPath = WhichUtil.Which(shellCommand, require: true, Trace, prependPath);
|
|
}
|
|
}
|
|
#else
|
|
shellCommand = "sh";
|
|
if (validateShellOnHost)
|
|
{
|
|
shellCommandPath = WhichUtil.Which("bash", false, Trace, prependPath) ?? WhichUtil.Which("sh", true, Trace, prependPath);
|
|
}
|
|
#endif
|
|
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
|
|
}
|
|
else
|
|
{
|
|
var parsed = ScriptHandlerHelpers.ParseShellOptionString(shell);
|
|
shellCommand = parsed.shellCommand;
|
|
if (validateShellOnHost)
|
|
{
|
|
shellCommandPath = WhichUtil.Which(parsed.shellCommand, true, Trace, prependPath);
|
|
}
|
|
|
|
argFormat = $"{parsed.shellArgs}".TrimStart();
|
|
if (string.IsNullOrEmpty(argFormat))
|
|
{
|
|
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(shellCommandPath))
|
|
{
|
|
ExecutionContext.Output($"shell: {shellCommandPath} {argFormat}");
|
|
}
|
|
else
|
|
{
|
|
ExecutionContext.Output($"shell: {shellCommand} {argFormat}");
|
|
}
|
|
|
|
if (this.Environment?.Count > 0)
|
|
{
|
|
ExecutionContext.Output("env:");
|
|
foreach (var env in this.Environment)
|
|
{
|
|
ExecutionContext.Output($" {env.Key}: {env.Value}");
|
|
}
|
|
}
|
|
|
|
ExecutionContext.Output("##[endgroup]");
|
|
}
|
|
|
|
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;
|
|
|
|
string workingDirectory = null;
|
|
if (!Inputs.TryGetValue("workingDirectory", out workingDirectory))
|
|
{
|
|
// Don't use job level working directories for hooks
|
|
if (IsActionStep && string.IsNullOrEmpty(ExecutionContext.ScopeName) && ExecutionContext.Global.JobDefaults.TryGetValue("run", out var runDefaults))
|
|
{
|
|
if (runDefaults.TryGetValue("working-directory", out workingDirectory))
|
|
{
|
|
ExecutionContext.Debug("Overwrite 'working-directory' base on job defaults.");
|
|
}
|
|
}
|
|
}
|
|
var workspaceDir = githubContext["workspace"] as StringContextData;
|
|
workingDirectory = Path.Combine(workspaceDir, workingDirectory ?? string.Empty);
|
|
|
|
string shell = null;
|
|
if (!Inputs.TryGetValue("shell", out shell) || string.IsNullOrEmpty(shell))
|
|
{
|
|
if (string.IsNullOrEmpty(ExecutionContext.ScopeName) && ExecutionContext.Global.JobDefaults.TryGetValue("run", out var runDefaults))
|
|
{
|
|
if (runDefaults.TryGetValue("shell", out shell))
|
|
{
|
|
ExecutionContext.Debug("Overwrite 'shell' base on job defaults.");
|
|
}
|
|
}
|
|
}
|
|
|
|
var isContainerStepHost = StepHost is ContainerStepHost;
|
|
|
|
string prependPath = string.Join(Path.PathSeparator.ToString(), ExecutionContext.Global.PrependPath.Reverse<string>());
|
|
string commandPath, argFormat, shellCommand;
|
|
// Set up default command and arguments
|
|
if (string.IsNullOrEmpty(shell))
|
|
{
|
|
#if OS_WINDOWS
|
|
shellCommand = "pwsh";
|
|
commandPath = WhichUtil.Which(shellCommand, require: false, Trace, prependPath);
|
|
if (string.IsNullOrEmpty(commandPath))
|
|
{
|
|
shellCommand = "powershell";
|
|
Trace.Info($"Defaulting to {shellCommand}");
|
|
commandPath = WhichUtil.Which(shellCommand, require: true, Trace, prependPath);
|
|
}
|
|
ArgUtil.NotNullOrEmpty(commandPath, "Default Shell");
|
|
#else
|
|
shellCommand = "sh";
|
|
commandPath = WhichUtil.Which("bash", false, Trace, prependPath) ?? WhichUtil.Which("sh", true, Trace, prependPath);
|
|
#endif
|
|
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
|
|
}
|
|
else
|
|
{
|
|
// For these shells, we want to use system binaries
|
|
var systemShells = new string[] { "bash", "sh", "powershell", "pwsh" };
|
|
if (!IsActionStep && systemShells.Contains(shell))
|
|
{
|
|
shellCommand = shell;
|
|
commandPath = WhichUtil.Which(shell, !isContainerStepHost, Trace, prependPath);
|
|
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
|
|
commandPath = WhichUtil.Which(parsed.shellCommand, !isContainerStepHost, Trace, prependPath);
|
|
argFormat = $"{parsed.shellArgs}".TrimStart();
|
|
if (string.IsNullOrEmpty(argFormat))
|
|
{
|
|
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Don't override runner telemetry here
|
|
if (!string.IsNullOrEmpty(shellCommand) && IsActionStep)
|
|
{
|
|
ExecutionContext.StepTelemetry.Action = shellCommand;
|
|
}
|
|
|
|
// 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}'");
|
|
}
|
|
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)}");
|
|
resolvedScriptPath = StepHost.ResolvePathForStepHost(ExecutionContext, scriptFilePath).Replace("\"", "\\\"");
|
|
}
|
|
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("\"", "\\\"");
|
|
}
|
|
|
|
// 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");
|
|
var encoding = ExecutionContext.Global.Variables.Retain_Default_Encoding && Console.InputEncoding.CodePage != 65001
|
|
? 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);
|
|
#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);
|
|
}
|
|
|
|
// 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;
|
|
#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
|
|
string node = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), NodeUtil.GetInternalNodeVersion(), "bin", $"node{IOUtil.ExeExtension}");
|
|
string macOSRunInvoker = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Bin), "macos-run-invoker.js");
|
|
arguments = $"\"{macOSRunInvoker.Replace("\"", "\\\"")}\" \"{fileName.Replace("\"", "\\\"")}\" {arguments}";
|
|
fileName = node;
|
|
}
|
|
#endif
|
|
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];
|
|
}
|
|
|
|
ExecutionContext.Debug($"{fileName} {arguments}");
|
|
|
|
Inputs.TryGetValue("standardInInput", out var standardInInput);
|
|
using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager))
|
|
using (var stderrManager = new OutputManager(ExecutionContext, ActionCommandManager))
|
|
{
|
|
StepHost.OutputDataReceived += stdoutManager.OnDataReceived;
|
|
StepHost.ErrorDataReceived += stderrManager.OnDataReceived;
|
|
|
|
// Execute
|
|
int exitCode = await StepHost.ExecuteAsync(ExecutionContext,
|
|
workingDirectory: StepHost.ResolvePathForStepHost(ExecutionContext, workingDirectory),
|
|
fileName: fileName,
|
|
arguments: arguments,
|
|
environment: Environment,
|
|
requireExitCodeZero: false,
|
|
outputEncoding: null,
|
|
killProcessOnCancel: false,
|
|
inheritConsoleHandler: !ExecutionContext.Global.Variables.Retain_Default_Encoding,
|
|
standardInInput: standardInInput,
|
|
cancellationToken: ExecutionContext.CancellationToken);
|
|
|
|
// Error
|
|
if (exitCode != 0)
|
|
{
|
|
ExecutionContext.Error($"Process completed with exit code {exitCode}.");
|
|
ExecutionContext.Result = TaskResult.Failed;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|