mirror of
https://github.com/actions/runner.git
synced 2025-12-10 12:36:23 +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>
421 lines
19 KiB
C#
421 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using GitHub.DistributedTask.ObjectTemplating;
|
|
using GitHub.DistributedTask.ObjectTemplating.Tokens;
|
|
using GitHub.DistributedTask.Pipelines;
|
|
using GitHub.DistributedTask.Pipelines.ContextData;
|
|
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
|
|
using GitHub.Runner.Common;
|
|
using GitHub.Runner.Common.Util;
|
|
using GitHub.Runner.Sdk;
|
|
using GitHub.Runner.Worker.Handlers;
|
|
using Pipelines = GitHub.DistributedTask.Pipelines;
|
|
|
|
namespace GitHub.Runner.Worker
|
|
{
|
|
public enum ActionRunStage
|
|
{
|
|
Pre,
|
|
Main,
|
|
Post,
|
|
}
|
|
|
|
[ServiceLocator(Default = typeof(ActionRunner))]
|
|
public interface IActionRunner : IStep, IRunnerService
|
|
{
|
|
ActionRunStage Stage { get; set; }
|
|
bool TryEvaluateDisplayName(DictionaryContextData contextData, IExecutionContext context);
|
|
Pipelines.ActionStep Action { get; set; }
|
|
}
|
|
|
|
public sealed class ActionRunner : RunnerService, IActionRunner
|
|
{
|
|
private bool _didFullyEvaluateDisplayName = false;
|
|
|
|
private string _displayName;
|
|
|
|
public ActionRunStage Stage { get; set; }
|
|
|
|
public string Condition { get; set; }
|
|
|
|
public TemplateToken ContinueOnError => Action?.ContinueOnError;
|
|
|
|
public string DisplayName
|
|
{
|
|
get
|
|
{
|
|
// TODO: remove the Action.DisplayName check post m158 deploy, it is done for back compat for older servers
|
|
if (!string.IsNullOrEmpty(Action?.DisplayName))
|
|
{
|
|
return Action?.DisplayName;
|
|
}
|
|
return string.IsNullOrEmpty(_displayName) ? "run" : _displayName;
|
|
}
|
|
set
|
|
{
|
|
_displayName = value;
|
|
}
|
|
}
|
|
|
|
public IExecutionContext ExecutionContext { get; set; }
|
|
|
|
public Pipelines.ActionStep Action { get; set; }
|
|
|
|
public TemplateToken Timeout => Action?.TimeoutInMinutes;
|
|
|
|
public async Task RunAsync()
|
|
{
|
|
// Validate args.
|
|
Trace.Entering();
|
|
ArgUtil.NotNull(ExecutionContext, nameof(ExecutionContext));
|
|
ArgUtil.NotNull(Action, nameof(Action));
|
|
var taskManager = HostContext.GetService<IActionManager>();
|
|
var handlerFactory = HostContext.GetService<IHandlerFactory>();
|
|
|
|
// Load the task definition and choose the handler.
|
|
Definition definition = taskManager.LoadAction(ExecutionContext, Action);
|
|
ArgUtil.NotNull(definition, nameof(definition));
|
|
|
|
ActionExecutionData handlerData = definition.Data?.Execution;
|
|
ArgUtil.NotNull(handlerData, nameof(handlerData));
|
|
|
|
List<JobExtensionRunner> localActionContainerSetupSteps = null;
|
|
// Handle Composite Local Actions
|
|
// Need to download and expand the tree of referenced actions
|
|
if (handlerData.ExecutionType == ActionExecutionType.Composite &&
|
|
handlerData is CompositeActionExecutionData compositeHandlerData &&
|
|
Stage == ActionRunStage.Main &&
|
|
Action.Reference is Pipelines.RepositoryPathReference localAction &&
|
|
string.Equals(localAction.RepositoryType, Pipelines.PipelineConstants.SelfAlias, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
var actionManager = HostContext.GetService<IActionManager>();
|
|
var prepareResult = await actionManager.PrepareActionsAsync(ExecutionContext, compositeHandlerData.Steps, ExecutionContext.Id);
|
|
|
|
// Reload definition since post may exist now (from embedded steps that were JIT downloaded)
|
|
definition = taskManager.LoadAction(ExecutionContext, Action);
|
|
ArgUtil.NotNull(definition, nameof(definition));
|
|
handlerData = definition.Data?.Execution;
|
|
ArgUtil.NotNull(handlerData, nameof(handlerData));
|
|
|
|
// Save container setup steps so we can reference them later
|
|
localActionContainerSetupSteps = prepareResult.ContainerSetupSteps;
|
|
}
|
|
|
|
if (handlerData.HasPre &&
|
|
Action.Reference is Pipelines.RepositoryPathReference repoAction &&
|
|
string.Equals(repoAction.RepositoryType, Pipelines.PipelineConstants.SelfAlias, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
ExecutionContext.Warning($"`pre` execution is not supported for local action from '{repoAction.Path}'");
|
|
}
|
|
|
|
// The action has post cleanup defined.
|
|
// we need to create timeline record for them and add them to the step list that StepRunner is using
|
|
if (handlerData.HasPost && (Stage == ActionRunStage.Pre || Stage == ActionRunStage.Main))
|
|
{
|
|
string postDisplayName = $"Post {this.DisplayName}";
|
|
if (Stage == ActionRunStage.Pre &&
|
|
this.DisplayName.StartsWith("Pre ", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// Trim the leading `Pre ` from the display name.
|
|
// Otherwise, we will get `Post Pre xxx` as DisplayName for the Post step.
|
|
postDisplayName = $"Post {this.DisplayName.Substring("Pre ".Length)}";
|
|
}
|
|
var repositoryReference = Action.Reference as RepositoryPathReference;
|
|
var pathString = string.IsNullOrEmpty(repositoryReference.Path) ? string.Empty : $"/{repositoryReference.Path}";
|
|
var repoString = string.IsNullOrEmpty(repositoryReference.Ref) ? $"{repositoryReference.Name}{pathString}" :
|
|
$"{repositoryReference.Name}{pathString}@{repositoryReference.Ref}";
|
|
|
|
ExecutionContext.Debug($"Register post job cleanup for action: {repoString}");
|
|
|
|
var actionRunner = HostContext.CreateService<IActionRunner>();
|
|
actionRunner.Action = Action;
|
|
actionRunner.Stage = ActionRunStage.Post;
|
|
actionRunner.Condition = handlerData.CleanupCondition;
|
|
actionRunner.DisplayName = postDisplayName;
|
|
|
|
ExecutionContext.RegisterPostJobStep(actionRunner);
|
|
}
|
|
|
|
IStepHost stepHost = HostContext.CreateService<IDefaultStepHost>();
|
|
|
|
ExecutionContext.WriteWebhookPayload();
|
|
|
|
// Set GITHUB_ACTION_REPOSITORY if this Action is from a repository
|
|
if (Action.Reference is Pipelines.RepositoryPathReference repoPathReferenceAction &&
|
|
!string.Equals(repoPathReferenceAction.RepositoryType, Pipelines.PipelineConstants.SelfAlias, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
ExecutionContext.SetGitHubContext("action_repository", repoPathReferenceAction.Name);
|
|
ExecutionContext.SetGitHubContext("action_ref", repoPathReferenceAction.Ref);
|
|
}
|
|
else
|
|
{
|
|
ExecutionContext.SetGitHubContext("action_repository", null);
|
|
ExecutionContext.SetGitHubContext("action_ref", null);
|
|
}
|
|
|
|
// Setup container stephost for running inside the container.
|
|
if (ExecutionContext.Global.Container != null)
|
|
{
|
|
// Make sure the required container is already created
|
|
// Container hooks do not necessarily set 'ContainerId'
|
|
if (!FeatureManager.IsContainerHooksEnabled(ExecutionContext.Global.Variables))
|
|
{
|
|
ArgUtil.NotNullOrEmpty(ExecutionContext.Global.Container.ContainerId, nameof(ExecutionContext.Global.Container.ContainerId));
|
|
}
|
|
var containerStepHost = HostContext.CreateService<IContainerStepHost>();
|
|
containerStepHost.Container = ExecutionContext.Global.Container;
|
|
stepHost = containerStepHost;
|
|
}
|
|
|
|
// Setup File Command Manager
|
|
var fileCommandManager = HostContext.CreateService<IFileCommandManager>();
|
|
fileCommandManager.InitializeFiles(ExecutionContext, null);
|
|
|
|
// Load the inputs.
|
|
ExecutionContext.Debug("Loading inputs");
|
|
Dictionary<string, string> inputs;
|
|
if (ExecutionContext.Global.Variables.GetBoolean(Constants.Runner.Features.UseContainerPathForTemplate) ?? false)
|
|
{
|
|
inputs = EvaluateStepInputs(stepHost);
|
|
}
|
|
else
|
|
{
|
|
var templateEvaluator = ExecutionContext.ToPipelineTemplateEvaluator();
|
|
inputs = templateEvaluator.EvaluateStepInputs(Action.Inputs, ExecutionContext.ExpressionValues, ExecutionContext.ExpressionFunctions);
|
|
}
|
|
|
|
var userInputs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
foreach (KeyValuePair<string, string> input in inputs)
|
|
{
|
|
userInputs.Add(input.Key);
|
|
string message = "";
|
|
if (definition.Data?.Deprecated?.TryGetValue(input.Key, out message) == true)
|
|
{
|
|
ExecutionContext.Warning(String.Format("Input '{0}' has been deprecated with message: {1}", input.Key, message));
|
|
}
|
|
}
|
|
|
|
var validInputs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
if (handlerData.ExecutionType == ActionExecutionType.Container)
|
|
{
|
|
// container action always accept 'entryPoint' and 'args' as inputs
|
|
// https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstepswithargs
|
|
validInputs.Add("entryPoint");
|
|
validInputs.Add("args");
|
|
}
|
|
// Merge the default inputs from the definition
|
|
if (definition.Data?.Inputs != null)
|
|
{
|
|
var manifestManager = HostContext.GetService<IActionManifestManager>();
|
|
foreach (var input in definition.Data.Inputs)
|
|
{
|
|
string key = input.Key.AssertString("action input name").Value;
|
|
validInputs.Add(key);
|
|
if (!inputs.ContainsKey(key))
|
|
{
|
|
inputs[key] = manifestManager.EvaluateDefaultInput(ExecutionContext, key, input.Value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate inputs only for actions with action.yml
|
|
if (Action.Reference.Type == Pipelines.ActionSourceType.Repository)
|
|
{
|
|
var unexpectedInputs = new List<string>();
|
|
foreach (var input in userInputs)
|
|
{
|
|
if (!validInputs.Contains(input))
|
|
{
|
|
unexpectedInputs.Add(input);
|
|
}
|
|
}
|
|
|
|
if (unexpectedInputs.Count > 0)
|
|
{
|
|
ExecutionContext.Warning($"Unexpected input(s) '{string.Join("', '", unexpectedInputs)}', valid inputs are ['{string.Join("', '", validInputs)}']");
|
|
}
|
|
}
|
|
|
|
// Load the action environment.
|
|
ExecutionContext.Debug("Loading env");
|
|
var environment = new Dictionary<String, String>(VarUtil.EnvironmentVariableKeyComparer);
|
|
|
|
#if OS_WINDOWS
|
|
var envContext = ExecutionContext.ExpressionValues["env"] as DictionaryContextData;
|
|
#else
|
|
var envContext = ExecutionContext.ExpressionValues["env"] as CaseSensitiveDictionaryContextData;
|
|
#endif
|
|
// Apply environment from env context, env context contains job level env and action's evn block
|
|
foreach (var env in envContext)
|
|
{
|
|
environment[env.Key] = env.Value.ToString();
|
|
}
|
|
|
|
// Apply action's intra-action state at last
|
|
foreach (var state in ExecutionContext.IntraActionState)
|
|
{
|
|
environment[$"STATE_{state.Key}"] = state.Value ?? string.Empty;
|
|
}
|
|
|
|
// Create the handler.
|
|
IHandler handler = handlerFactory.Create(
|
|
ExecutionContext,
|
|
Action.Reference,
|
|
stepHost,
|
|
handlerData,
|
|
inputs,
|
|
environment,
|
|
ExecutionContext.Global.Variables,
|
|
actionDirectory: definition.Directory,
|
|
localActionContainerSetupSteps: localActionContainerSetupSteps);
|
|
|
|
// Print out action details and log telemetry
|
|
handler.PrepareExecution(Stage);
|
|
|
|
// Run the task.
|
|
try
|
|
{
|
|
await handler.RunAsync(Stage);
|
|
}
|
|
finally
|
|
{
|
|
fileCommandManager.ProcessFiles(ExecutionContext, ExecutionContext.Global.Container);
|
|
}
|
|
|
|
}
|
|
|
|
public bool TryEvaluateDisplayName(DictionaryContextData contextData, IExecutionContext context)
|
|
{
|
|
ArgUtil.NotNull(context, nameof(context));
|
|
ArgUtil.NotNull(Action, nameof(Action));
|
|
|
|
// If we have already expanded the display name, there is no need to expand it again
|
|
// TODO: Remove the ShouldEvaluateDisplayName check and field post m158 deploy, we should do it by default once the server is updated
|
|
if (_didFullyEvaluateDisplayName || !string.IsNullOrEmpty(Action.DisplayName))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool didFullyEvaluate;
|
|
_displayName = GenerateDisplayName(Action, contextData, context, out didFullyEvaluate);
|
|
|
|
// If we evaluated fully mask any secrets
|
|
if (didFullyEvaluate)
|
|
{
|
|
_displayName = HostContext.SecretMasker.MaskSecrets(_displayName);
|
|
}
|
|
context.Debug($"Set step '{Action.Name}' display name to: '{_displayName}'");
|
|
_didFullyEvaluateDisplayName = didFullyEvaluate;
|
|
return didFullyEvaluate;
|
|
}
|
|
|
|
private Dictionary<String, String> EvaluateStepInputs(IStepHost stepHost)
|
|
{
|
|
DictionaryContextData expressionValues = ExecutionContext.GetExpressionValues(stepHost);
|
|
var templateEvaluator = ExecutionContext.ToPipelineTemplateEvaluator();
|
|
var inputs = templateEvaluator.EvaluateStepInputs(Action.Inputs, expressionValues, ExecutionContext.ExpressionFunctions);
|
|
|
|
return inputs;
|
|
}
|
|
|
|
private string GenerateDisplayName(ActionStep action, DictionaryContextData contextData, IExecutionContext context, out bool didFullyEvaluate)
|
|
{
|
|
ArgUtil.NotNull(context, nameof(context));
|
|
ArgUtil.NotNull(action, nameof(action));
|
|
|
|
var displayName = string.Empty;
|
|
var prefix = string.Empty;
|
|
var tokenToParse = default(ScalarToken);
|
|
didFullyEvaluate = false;
|
|
// Get the token we need to parse
|
|
// It could be passed in as the Display Name, or we have to pull it from various parts of the Action.
|
|
if (action.DisplayNameToken != null)
|
|
{
|
|
tokenToParse = action.DisplayNameToken as ScalarToken;
|
|
}
|
|
else if (action.Reference?.Type == ActionSourceType.Repository)
|
|
{
|
|
prefix = PipelineTemplateConstants.RunDisplayPrefix;
|
|
var repositoryReference = action.Reference as RepositoryPathReference;
|
|
var pathString = string.IsNullOrEmpty(repositoryReference.Path) ? string.Empty : $"/{repositoryReference.Path}";
|
|
var repoString = string.IsNullOrEmpty(repositoryReference.Ref) ? $"{repositoryReference.Name}{pathString}" :
|
|
$"{repositoryReference.Name}{pathString}@{repositoryReference.Ref}";
|
|
tokenToParse = new StringToken(null, null, null, repoString);
|
|
}
|
|
else if (action.Reference?.Type == ActionSourceType.ContainerRegistry)
|
|
{
|
|
prefix = PipelineTemplateConstants.RunDisplayPrefix;
|
|
var containerReference = action.Reference as ContainerRegistryReference;
|
|
tokenToParse = new StringToken(null, null, null, containerReference.Image);
|
|
}
|
|
else if (action.Reference?.Type == ActionSourceType.Script)
|
|
{
|
|
prefix = PipelineTemplateConstants.RunDisplayPrefix;
|
|
var inputs = action.Inputs.AssertMapping(null);
|
|
foreach (var pair in inputs)
|
|
{
|
|
var propertyName = pair.Key.AssertString($"{PipelineTemplateConstants.Steps}");
|
|
if (string.Equals(propertyName.Value, "script", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
tokenToParse = pair.Value.AssertScalar($"{PipelineTemplateConstants.Steps} item {PipelineTemplateConstants.Run}");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
context.Error($"Encountered an unknown action reference type when evaluating the display name: {action.Reference?.Type}");
|
|
return displayName;
|
|
}
|
|
|
|
// If we have nothing to parse, abort
|
|
if (tokenToParse == null)
|
|
{
|
|
return displayName;
|
|
}
|
|
// Try evaluating fully
|
|
try
|
|
{
|
|
if (tokenToParse.CheckHasRequiredContext(contextData, context.ExpressionFunctions))
|
|
{
|
|
var templateEvaluator = context.ToPipelineTemplateEvaluator();
|
|
displayName = templateEvaluator.EvaluateStepDisplayName(tokenToParse, contextData, context.ExpressionFunctions);
|
|
didFullyEvaluate = true;
|
|
}
|
|
}
|
|
catch (TemplateValidationException e)
|
|
{
|
|
context.Warning($"Encountered an error when evaluating display name {tokenToParse.ToString()}. {e.Message}");
|
|
return displayName;
|
|
}
|
|
|
|
// Default to a prettified token if we could not evaluate
|
|
if (!didFullyEvaluate)
|
|
{
|
|
displayName = tokenToParse.ToDisplayString();
|
|
}
|
|
|
|
displayName = FormatStepName(prefix, displayName);
|
|
return displayName;
|
|
}
|
|
|
|
private static string FormatStepName(string prefix, string stepName)
|
|
{
|
|
if (string.IsNullOrEmpty(stepName))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var result = stepName.TrimStart(' ', '\t', '\r', '\n');
|
|
var firstNewLine = result.IndexOfAny(new[] { '\r', '\n' });
|
|
if (firstNewLine >= 0)
|
|
{
|
|
result = result.Substring(0, firstNewLine);
|
|
}
|
|
return $"{prefix}{result}";
|
|
}
|
|
}
|
|
}
|