diff --git a/src/Runner.Worker/ActionManager.cs b/src/Runner.Worker/ActionManager.cs index 242ab79e0..65c3d3593 100644 --- a/src/Runner.Worker/ActionManager.cs +++ b/src/Runner.Worker/ActionManager.cs @@ -398,8 +398,10 @@ namespace GitHub.Runner.Worker else if (definition.Data.Execution.ExecutionType == ActionExecutionType.Composite && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TESTING_COMPOSITE_ACTIONS_ALPHA"))) { var compositeAction = definition.Data.Execution as CompositeActionExecutionData; - Trace.Info($"Load {compositeAction.Steps.Count} action steps."); - Trace.Verbose($"Details: {StringUtil.ConvertToJson(compositeAction.Steps)}"); + Trace.Info($"Load {compositeAction.Steps?.Count ?? 0} action steps."); + Trace.Verbose($"Details: {StringUtil.ConvertToJson(compositeAction?.Steps)}"); + Trace.Info($"Load: {compositeAction.Outputs?.Count ?? 0} number of outputs"); + Trace.Info($"Details: {StringUtil.ConvertToJson(compositeAction?.Outputs)}"); } else { @@ -1222,6 +1224,7 @@ namespace GitHub.Runner.Worker public override bool HasPre => false; public override bool HasPost => false; public List Steps { get; set; } + public MappingToken Outputs { get; set; } } public abstract class ActionExecutionData diff --git a/src/Runner.Worker/ActionManifestManager.cs b/src/Runner.Worker/ActionManifestManager.cs index ae5ca73d7..a7be7cb17 100644 --- a/src/Runner.Worker/ActionManifestManager.cs +++ b/src/Runner.Worker/ActionManifestManager.cs @@ -23,11 +23,15 @@ namespace GitHub.Runner.Worker { ActionDefinitionData Load(IExecutionContext executionContext, string manifestFile); + DictionaryContextData EvaluateCompositeOutputs(IExecutionContext executionContext, TemplateToken token, IDictionary extraExpressionValues); + List EvaluateContainerArguments(IExecutionContext executionContext, SequenceToken token, IDictionary extraExpressionValues); Dictionary EvaluateContainerEnvironment(IExecutionContext executionContext, MappingToken token, IDictionary extraExpressionValues); string EvaluateDefaultInput(IExecutionContext executionContext, string inputName, TemplateToken token); + + void SetAllCompositeOutputs(IExecutionContext parentExecutionContext, DictionaryContextData actionOutputs); } public sealed class ActionManifestManager : RunnerService, IActionManifestManager @@ -89,6 +93,9 @@ namespace GitHub.Runner.Worker } var actionMapping = token.AssertMapping("action manifest root"); + var actionOutputs = default(MappingToken); + var actionRunValueToken = default(TemplateToken); + foreach (var actionPair in actionMapping) { var propertyName = actionPair.Key.AssertString($"action.yml property key"); @@ -99,6 +106,15 @@ namespace GitHub.Runner.Worker actionDefinition.Name = actionPair.Value.AssertString("name").Value; break; + case "outputs": + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TESTING_COMPOSITE_ACTIONS_ALPHA"))) + { + actionOutputs = actionPair.Value.AssertMapping("outputs"); + break; + } + Trace.Info($"Ignore action property outputs. Outputs for a whole action is not supported yet."); + break; + case "description": actionDefinition.Description = actionPair.Value.AssertString("description").Value; break; @@ -108,13 +124,21 @@ namespace GitHub.Runner.Worker break; case "runs": - actionDefinition.Execution = ConvertRuns(executionContext, templateContext, actionPair.Value); + // Defer runs token evaluation to after for loop to ensure that order of outputs doesn't matter. + actionRunValueToken = actionPair.Value; break; + default: Trace.Info($"Ignore action property {propertyName}."); break; } } + + // Evaluate Runs Last + if (actionRunValueToken != null) + { + actionDefinition.Execution = ConvertRuns(executionContext, templateContext, actionRunValueToken, actionOutputs); + } } catch (Exception ex) { @@ -146,6 +170,61 @@ namespace GitHub.Runner.Worker return actionDefinition; } + public void SetAllCompositeOutputs( + IExecutionContext parentExecutionContext, + DictionaryContextData actionOutputs) + { + // Each pair is structured like this + // We ignore "description" for now + // { + // "the-output-name": { + // "description": "", + // "value": "the value" + // }, + // ... + // } + foreach (var pair in actionOutputs) + { + var outputsName = pair.Key; + var outputsAttributes = pair.Value as DictionaryContextData; + outputsAttributes.TryGetValue("value", out var val); + var outputsValue = val as StringContextData; + + // Set output in the whole composite scope. + if (!String.IsNullOrEmpty(outputsName) && !String.IsNullOrEmpty(outputsValue)) + { + parentExecutionContext.SetOutput(outputsName, outputsValue, out _); + } + } + } + + public DictionaryContextData EvaluateCompositeOutputs( + IExecutionContext executionContext, + TemplateToken token, + IDictionary extraExpressionValues) + { + var result = default(DictionaryContextData); + + if (token != null) + { + var context = CreateContext(executionContext, extraExpressionValues); + try + { + token = TemplateEvaluator.Evaluate(context, "outputs", token, 0, null, omitHeader: true); + context.Errors.Check(); + result = token.ToContextData().AssertDictionary("composite outputs"); + } + catch (Exception ex) when (!(ex is TemplateValidationException)) + { + context.Errors.Add(ex); + } + + context.Errors.Check(); + } + + return result ?? new DictionaryContextData(); + } + public List EvaluateContainerArguments( IExecutionContext executionContext, SequenceToken token, @@ -309,7 +388,8 @@ namespace GitHub.Runner.Worker private ActionExecutionData ConvertRuns( IExecutionContext executionContext, TemplateContext context, - TemplateToken inputsToken) + TemplateToken inputsToken, + MappingToken outputs = null) { var runsMapping = inputsToken.AssertMapping("runs"); var usingToken = default(StringToken); @@ -439,6 +519,7 @@ namespace GitHub.Runner.Worker return new CompositeActionExecutionData() { Steps = stepsLoaded, + Outputs = outputs }; } } diff --git a/src/Runner.Worker/ExecutionContext.cs b/src/Runner.Worker/ExecutionContext.cs index 83e47c74c..23b26aee1 100644 --- a/src/Runner.Worker/ExecutionContext.cs +++ b/src/Runner.Worker/ExecutionContext.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using System.Web; @@ -52,7 +53,6 @@ namespace GitHub.Runner.Worker IDictionary> JobDefaults { get; } Dictionary JobOutputs { get; } IDictionary EnvironmentVariables { get; } - IDictionary Scopes { get; } IList FileTable { get; } StepsContext StepsContext { get; } DictionaryContextData ExpressionValues { get; } @@ -70,6 +70,8 @@ namespace GitHub.Runner.Worker bool EchoOnActionCommand { get; set; } + IExecutionContext FinalizeContext { get; set; } + // Initialize void InitializeJob(Pipelines.AgentJobRequestMessage message, CancellationToken token); void CancelToken(); @@ -105,7 +107,7 @@ namespace GitHub.Runner.Worker // others void ForceTaskComplete(); void RegisterPostJobStep(IStep step); - void RegisterNestedStep(IStep step, DictionaryContextData inputsData, int location, Dictionary envData); + IStep RegisterNestedStep(IActionRunner step, DictionaryContextData inputsData, int location, Dictionary envData, bool cleanUp = false); } public sealed class ExecutionContext : RunnerService, IExecutionContext @@ -120,6 +122,9 @@ namespace GitHub.Runner.Worker private event OnMatcherChanged _onMatcherChanged; + // Regex used for checking if ScopeName meets the condition that shows that its id is null. + private readonly static Regex _generatedContextNamePattern = new Regex("^__[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$", RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); + private IssueMatcherConfig[] _matchers; private IPagingLogger _logger; @@ -149,7 +154,6 @@ namespace GitHub.Runner.Worker public IDictionary> JobDefaults { get; private set; } public Dictionary JobOutputs { get; private set; } public IDictionary EnvironmentVariables { get; private set; } - public IDictionary Scopes { get; private set; } public IList FileTable { get; private set; } public StepsContext StepsContext { get; private set; } public DictionaryContextData ExpressionValues { get; } = new DictionaryContextData(); @@ -170,6 +174,8 @@ namespace GitHub.Runner.Worker public bool EchoOnActionCommand { get; set; } + public IExecutionContext FinalizeContext { get; set; } + public TaskResult? Result { get @@ -270,17 +276,36 @@ namespace GitHub.Runner.Worker /// Helper function used in CompositeActionHandler::RunAsync to /// add a child node, aka a step, to the current job to the Root.JobSteps based on the location. /// - public void RegisterNestedStep(IStep step, DictionaryContextData inputsData, int location, Dictionary envData) + public IStep RegisterNestedStep( + IActionRunner step, + DictionaryContextData inputsData, + int location, + Dictionary envData, + bool cleanUp = false) { // TODO: For UI purposes, look at figuring out how to condense steps in one node => maybe use the same previous GUID var newGuid = Guid.NewGuid(); - step.ExecutionContext = Root.CreateChild(newGuid, step.DisplayName, newGuid.ToString("N"), null, null); + + // If the context name is empty and the scope name is empty, we would generate a unique scope name for this child in the following format: + // "__" + var safeContextName = !string.IsNullOrEmpty(ContextName) ? ContextName : $"__{newGuid}"; + + // Set Scope Name. Note, for our design, we consider each step in a composite action to have the same scope + // This makes it much simpler to handle their outputs at the end of the Composite Action + var childScopeName = !string.IsNullOrEmpty(ScopeName) ? $"{ScopeName}.{safeContextName}" : safeContextName; + + var childContextName = !string.IsNullOrEmpty(step.Action.ContextName) ? step.Action.ContextName : $"__{Guid.NewGuid()}"; + + step.ExecutionContext = Root.CreateChild(newGuid, step.DisplayName, newGuid.ToString("N"), childScopeName, childContextName); step.ExecutionContext.ExpressionValues["inputs"] = inputsData; + // Set Parent Attribute for Clean Up Step + if (cleanUp) + { + step.ExecutionContext.FinalizeContext = this; + } + // Add the composite action environment variables to each step. - // If the key already exists, we override it since the composite action env variables will have higher precedence - // Note that for each composite action step, it's environment variables will be set in the StepRunner automatically - // step.ExecutionContext.SetEnvironmentVariables(envData); #if OS_WINDOWS var envContext = new DictionaryContextData(); #else @@ -293,6 +318,8 @@ namespace GitHub.Runner.Worker step.ExecutionContext.ExpressionValues["env"] = envContext; Root.JobSteps.Insert(location, step); + + return step; } public IExecutionContext CreateChild(Guid recordId, string displayName, string refName, string scopeName, string contextName, Dictionary intraActionState = null, int? recordOrder = null) @@ -317,7 +344,6 @@ namespace GitHub.Runner.Worker } child.EnvironmentVariables = EnvironmentVariables; child.JobDefaults = JobDefaults; - child.Scopes = Scopes; child.FileTable = FileTable; child.StepsContext = StepsContext; foreach (var pair in ExpressionValues) @@ -466,7 +492,8 @@ namespace GitHub.Runner.Worker { ArgUtil.NotNullOrEmpty(name, nameof(name)); - if (String.IsNullOrEmpty(ContextName)) + // if the ContextName follows the __GUID format which is set as the default value for ContextName if null for Composite Actions. + if (String.IsNullOrEmpty(ContextName) || _generatedContextNamePattern.IsMatch(ContextName)) { reference = null; return; @@ -633,16 +660,6 @@ namespace GitHub.Runner.Worker // Steps context (StepsRunner manages adding the scoped steps context) StepsContext = new StepsContext(); - // Scopes - Scopes = new Dictionary(StringComparer.OrdinalIgnoreCase); - if (message.Scopes?.Count > 0) - { - foreach (var scope in message.Scopes) - { - Scopes[scope.Name] = scope; - } - } - // File table FileTable = new List(message.FileTable ?? new string[0]); diff --git a/src/Runner.Worker/Handlers/CompositeActionHandler.cs b/src/Runner.Worker/Handlers/CompositeActionHandler.cs index ddf5821ad..4b6ebdd3b 100644 --- a/src/Runner.Worker/Handlers/CompositeActionHandler.cs +++ b/src/Runner.Worker/Handlers/CompositeActionHandler.cs @@ -47,6 +47,7 @@ namespace GitHub.Runner.Worker.Handlers // Add each composite action step to the front of the queue int location = 0; + foreach (Pipelines.ActionStep aStep in actionSteps) { // Ex: @@ -85,12 +86,35 @@ namespace GitHub.Runner.Worker.Handlers actionRunner.Condition = aStep.Condition; actionRunner.DisplayName = aStep.DisplayName; - ExecutionContext.RegisterNestedStep(actionRunner, inputsData, location, Environment); + var step = ExecutionContext.RegisterNestedStep(actionRunner, inputsData, location, Environment); + + InitializeScope(step); + location++; } + // Create a step that handles all the composite action steps' outputs + Pipelines.ActionStep cleanOutputsStep = new Pipelines.ActionStep(); + cleanOutputsStep.ContextName = ExecutionContext.ContextName; + cleanOutputsStep.DisplayName = "Composite Action Steps Cleanup"; + // Use the same reference type as our composite steps. + cleanOutputsStep.Reference = Action; + + var actionRunner2 = HostContext.CreateService(); + actionRunner2.Action = cleanOutputsStep; + actionRunner2.Stage = ActionRunStage.Main; + actionRunner2.Condition = "always()"; + actionRunner2.DisplayName = "Composite Action Steps Cleanup"; + ExecutionContext.RegisterNestedStep(actionRunner2, inputsData, location, Environment, true); + return Task.CompletedTask; } + private void InitializeScope(IStep step) + { + var stepsContext = step.ExecutionContext.StepsContext; + var scopeName = step.ExecutionContext.ScopeName; + step.ExecutionContext.ExpressionValues["steps"] = stepsContext.GetScope(scopeName); + } } } diff --git a/src/Runner.Worker/Handlers/CompositeActionOutputHandler.cs b/src/Runner.Worker/Handlers/CompositeActionOutputHandler.cs new file mode 100644 index 000000000..ea5241200 --- /dev/null +++ b/src/Runner.Worker/Handlers/CompositeActionOutputHandler.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using GitHub.DistributedTask.ObjectTemplating.Schema; +using GitHub.DistributedTask.ObjectTemplating.Tokens; +using GitHub.DistributedTask.Pipelines.ContextData; +using GitHub.DistributedTask.WebApi; +using GitHub.Runner.Common; +using GitHub.Runner.Sdk; +using Pipelines = GitHub.DistributedTask.Pipelines; + +namespace GitHub.Runner.Worker.Handlers +{ + [ServiceLocator(Default = typeof(CompositeActionOutputHandler))] + public interface ICompositeActionOutputHandler : IHandler + { + CompositeActionExecutionData Data { get; set; } + } + + public sealed class CompositeActionOutputHandler : Handler, ICompositeActionOutputHandler + { + public CompositeActionExecutionData Data { get; set; } + + + public Task RunAsync(ActionRunStage stage) + { + // Evaluate the mapped outputs value + if (Data.Outputs != null) + { + // Evaluate the outputs in the steps context to easily retrieve the values + var actionManifestManager = HostContext.GetService(); + + // Format ExpressionValues to Dictionary + var evaluateContext = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var pair in ExecutionContext.ExpressionValues) + { + evaluateContext[pair.Key] = pair.Value; + } + + // Get the evluated composite outputs' values mapped to the outputs named + DictionaryContextData actionOutputs = actionManifestManager.EvaluateCompositeOutputs(ExecutionContext, Data.Outputs, evaluateContext); + + // Set the outputs for the outputs object in the whole composite action + actionManifestManager.SetAllCompositeOutputs(ExecutionContext.FinalizeContext, actionOutputs); + } + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/src/Runner.Worker/Handlers/HandlerFactory.cs b/src/Runner.Worker/Handlers/HandlerFactory.cs index db4d6559c..4591ccab2 100644 --- a/src/Runner.Worker/Handlers/HandlerFactory.cs +++ b/src/Runner.Worker/Handlers/HandlerFactory.cs @@ -68,8 +68,16 @@ namespace GitHub.Runner.Worker.Handlers } else if (data.ExecutionType == ActionExecutionType.Composite) { - handler = HostContext.CreateService(); - (handler as ICompositeActionHandler).Data = data as CompositeActionExecutionData; + if (executionContext.FinalizeContext == null) + { + handler = HostContext.CreateService(); + (handler as ICompositeActionHandler).Data = data as CompositeActionExecutionData; + } + else + { + handler = HostContext.CreateService(); + (handler as ICompositeActionOutputHandler).Data = data as CompositeActionExecutionData; + } } else { diff --git a/src/Runner.Worker/StepsRunner.cs b/src/Runner.Worker/StepsRunner.cs index 966f73dc1..553f79248 100644 --- a/src/Runner.Worker/StepsRunner.cs +++ b/src/Runner.Worker/StepsRunner.cs @@ -67,7 +67,6 @@ namespace GitHub.Runner.Worker var step = jobContext.JobSteps[0]; jobContext.JobSteps.RemoveAt(0); - var nextStep = jobContext.JobSteps.Count > 0 ? jobContext.JobSteps[0] : null; Trace.Info($"Processing step: DisplayName='{step.DisplayName}'"); ArgUtil.NotNull(step.ExecutionContext, nameof(step.ExecutionContext)); @@ -83,171 +82,170 @@ namespace GitHub.Runner.Worker step.ExecutionContext.ExpressionFunctions.Add(new FunctionInfo(PipelineTemplateConstants.Success, 0, 0)); step.ExecutionContext.ExpressionFunctions.Add(new FunctionInfo(PipelineTemplateConstants.HashFiles, 1, byte.MaxValue)); - // Initialize scope - if (InitializeScope(step, scopeInputs)) - { - // Populate env context for each step - Trace.Info("Initialize Env context for step"); -#if OS_WINDOWS - var envContext = new DictionaryContextData(); -#else - var envContext = new CaseSensitiveDictionaryContextData(); -#endif - // Global env - foreach (var pair in step.ExecutionContext.EnvironmentVariables) - { - envContext[pair.Key] = new StringContextData(pair.Value ?? string.Empty); - } + step.ExecutionContext.ExpressionValues["steps"] = step.ExecutionContext.StepsContext.GetScope(step.ExecutionContext.ScopeName); - // Stomps over with outside step env - if (step.ExecutionContext.ExpressionValues.TryGetValue("env", out var envContextData)) - { + // Populate env context for each step + Trace.Info("Initialize Env context for step"); #if OS_WINDOWS - var dict = envContextData as DictionaryContextData; + var envContext = new DictionaryContextData(); #else - var dict = envContextData as CaseSensitiveDictionaryContextData; + var envContext = new CaseSensitiveDictionaryContextData(); #endif - foreach (var pair in dict) + + // Global env + foreach (var pair in step.ExecutionContext.EnvironmentVariables) + { + envContext[pair.Key] = new StringContextData(pair.Value ?? string.Empty); + } + + // Stomps over with outside step env + if (step.ExecutionContext.ExpressionValues.TryGetValue("env", out var envContextData)) + { +#if OS_WINDOWS + var dict = envContextData as DictionaryContextData; +#else + var dict = envContextData as CaseSensitiveDictionaryContextData; +#endif + foreach (var pair in dict) + { + envContext[pair.Key] = pair.Value; + } + } + + step.ExecutionContext.ExpressionValues["env"] = envContext; + + bool evaluateStepEnvFailed = false; + if (step is IActionRunner actionStep) + { + // Set GITHUB_ACTION + step.ExecutionContext.SetGitHubContext("action", actionStep.Action.Name); + + try + { + // Evaluate and merge action's env block to env context + var templateEvaluator = step.ExecutionContext.ToPipelineTemplateEvaluator(); + var actionEnvironment = templateEvaluator.EvaluateStepEnvironment(actionStep.Action.Environment, step.ExecutionContext.ExpressionValues, step.ExecutionContext.ExpressionFunctions, VarUtil.EnvironmentVariableKeyComparer); + foreach (var env in actionEnvironment) { - envContext[pair.Key] = pair.Value; + envContext[env.Key] = new StringContextData(env.Value ?? string.Empty); } } - - step.ExecutionContext.ExpressionValues["env"] = envContext; - - bool evaluateStepEnvFailed = false; - if (step is IActionRunner actionStep) + catch (Exception ex) { - // Set GITHUB_ACTION - step.ExecutionContext.SetGitHubContext("action", actionStep.Action.Name); + // fail the step since there is an evaluate error. + Trace.Info("Caught exception from expression for step.env"); + evaluateStepEnvFailed = true; + step.ExecutionContext.Error(ex); + CompleteStep(step, TaskResult.Failed); + } + } - try + if (!evaluateStepEnvFailed) + { + try + { + // Register job cancellation call back only if job cancellation token not been fire before each step run + if (!jobContext.CancellationToken.IsCancellationRequested) { - // Evaluate and merge action's env block to env context - var templateEvaluator = step.ExecutionContext.ToPipelineTemplateEvaluator(); - var actionEnvironment = templateEvaluator.EvaluateStepEnvironment(actionStep.Action.Environment, step.ExecutionContext.ExpressionValues, step.ExecutionContext.ExpressionFunctions, VarUtil.EnvironmentVariableKeyComparer); - foreach (var env in actionEnvironment) + // Test the condition again. The job was canceled after the condition was originally evaluated. + jobCancelRegister = jobContext.CancellationToken.Register(() => { - envContext[env.Key] = new StringContextData(env.Value ?? string.Empty); + // mark job as cancelled + jobContext.Result = TaskResult.Canceled; + jobContext.JobContext.Status = jobContext.Result?.ToActionResult(); + + step.ExecutionContext.Debug($"Re-evaluate condition on job cancellation for step: '{step.DisplayName}'."); + var conditionReTestTraceWriter = new ConditionTraceWriter(Trace, null); // host tracing only + var conditionReTestResult = false; + if (HostContext.RunnerShutdownToken.IsCancellationRequested) + { + step.ExecutionContext.Debug($"Skip Re-evaluate condition on runner shutdown."); + } + else + { + try + { + var templateEvaluator = step.ExecutionContext.ToPipelineTemplateEvaluator(conditionReTestTraceWriter); + var condition = new BasicExpressionToken(null, null, null, step.Condition); + conditionReTestResult = templateEvaluator.EvaluateStepIf(condition, step.ExecutionContext.ExpressionValues, step.ExecutionContext.ExpressionFunctions, step.ExecutionContext.ToExpressionState()); + } + catch (Exception ex) + { + // Cancel the step since we get exception while re-evaluate step condition. + Trace.Info("Caught exception from expression when re-test condition on job cancellation."); + step.ExecutionContext.Error(ex); + } + } + + if (!conditionReTestResult) + { + // Cancel the step. + Trace.Info("Cancel current running step."); + step.ExecutionContext.CancelToken(); + } + }); + } + else + { + if (jobContext.Result != TaskResult.Canceled) + { + // mark job as cancelled + jobContext.Result = TaskResult.Canceled; + jobContext.JobContext.Status = jobContext.Result?.ToActionResult(); } } - catch (Exception ex) + + // Evaluate condition. + step.ExecutionContext.Debug($"Evaluating condition for step: '{step.DisplayName}'"); + var conditionTraceWriter = new ConditionTraceWriter(Trace, step.ExecutionContext); + var conditionResult = false; + var conditionEvaluateError = default(Exception); + if (HostContext.RunnerShutdownToken.IsCancellationRequested) + { + step.ExecutionContext.Debug($"Skip evaluate condition on runner shutdown."); + } + else + { + try + { + var templateEvaluator = step.ExecutionContext.ToPipelineTemplateEvaluator(conditionTraceWriter); + var condition = new BasicExpressionToken(null, null, null, step.Condition); + conditionResult = templateEvaluator.EvaluateStepIf(condition, step.ExecutionContext.ExpressionValues, step.ExecutionContext.ExpressionFunctions, step.ExecutionContext.ToExpressionState()); + } + catch (Exception ex) + { + Trace.Info("Caught exception from expression."); + Trace.Error(ex); + conditionEvaluateError = ex; + } + } + + // no evaluate error but condition is false + if (!conditionResult && conditionEvaluateError == null) + { + // Condition == false + Trace.Info("Skipping step due to condition evaluation."); + CompleteStep(step, TaskResult.Skipped, resultCode: conditionTraceWriter.Trace); + } + else if (conditionEvaluateError != null) { // fail the step since there is an evaluate error. - Trace.Info("Caught exception from expression for step.env"); - evaluateStepEnvFailed = true; - step.ExecutionContext.Error(ex); - CompleteStep(step, nextStep, TaskResult.Failed); + step.ExecutionContext.Error(conditionEvaluateError); + CompleteStep(step, TaskResult.Failed); + } + else + { + // Run the step. + await RunStepAsync(step, jobContext.CancellationToken); + CompleteStep(step); } } - - if (!evaluateStepEnvFailed) + finally { - try + if (jobCancelRegister != null) { - // Register job cancellation call back only if job cancellation token not been fire before each step run - if (!jobContext.CancellationToken.IsCancellationRequested) - { - // Test the condition again. The job was canceled after the condition was originally evaluated. - jobCancelRegister = jobContext.CancellationToken.Register(() => - { - // mark job as cancelled - jobContext.Result = TaskResult.Canceled; - jobContext.JobContext.Status = jobContext.Result?.ToActionResult(); - - step.ExecutionContext.Debug($"Re-evaluate condition on job cancellation for step: '{step.DisplayName}'."); - var conditionReTestTraceWriter = new ConditionTraceWriter(Trace, null); // host tracing only - var conditionReTestResult = false; - if (HostContext.RunnerShutdownToken.IsCancellationRequested) - { - step.ExecutionContext.Debug($"Skip Re-evaluate condition on runner shutdown."); - } - else - { - try - { - var templateEvaluator = step.ExecutionContext.ToPipelineTemplateEvaluator(conditionReTestTraceWriter); - var condition = new BasicExpressionToken(null, null, null, step.Condition); - conditionReTestResult = templateEvaluator.EvaluateStepIf(condition, step.ExecutionContext.ExpressionValues, step.ExecutionContext.ExpressionFunctions, step.ExecutionContext.ToExpressionState()); - } - catch (Exception ex) - { - // Cancel the step since we get exception while re-evaluate step condition. - Trace.Info("Caught exception from expression when re-test condition on job cancellation."); - step.ExecutionContext.Error(ex); - } - } - - if (!conditionReTestResult) - { - // Cancel the step. - Trace.Info("Cancel current running step."); - step.ExecutionContext.CancelToken(); - } - }); - } - else - { - if (jobContext.Result != TaskResult.Canceled) - { - // mark job as cancelled - jobContext.Result = TaskResult.Canceled; - jobContext.JobContext.Status = jobContext.Result?.ToActionResult(); - } - } - - // Evaluate condition. - step.ExecutionContext.Debug($"Evaluating condition for step: '{step.DisplayName}'"); - var conditionTraceWriter = new ConditionTraceWriter(Trace, step.ExecutionContext); - var conditionResult = false; - var conditionEvaluateError = default(Exception); - if (HostContext.RunnerShutdownToken.IsCancellationRequested) - { - step.ExecutionContext.Debug($"Skip evaluate condition on runner shutdown."); - } - else - { - try - { - var templateEvaluator = step.ExecutionContext.ToPipelineTemplateEvaluator(conditionTraceWriter); - var condition = new BasicExpressionToken(null, null, null, step.Condition); - conditionResult = templateEvaluator.EvaluateStepIf(condition, step.ExecutionContext.ExpressionValues, step.ExecutionContext.ExpressionFunctions, step.ExecutionContext.ToExpressionState()); - } - catch (Exception ex) - { - Trace.Info("Caught exception from expression."); - Trace.Error(ex); - conditionEvaluateError = ex; - } - } - - // no evaluate error but condition is false - if (!conditionResult && conditionEvaluateError == null) - { - // Condition == false - Trace.Info("Skipping step due to condition evaluation."); - CompleteStep(step, nextStep, TaskResult.Skipped, resultCode: conditionTraceWriter.Trace); - } - else if (conditionEvaluateError != null) - { - // fail the step since there is an evaluate error. - step.ExecutionContext.Error(conditionEvaluateError); - CompleteStep(step, nextStep, TaskResult.Failed); - } - else - { - // Run the step. - await RunStepAsync(step, jobContext.CancellationToken); - CompleteStep(step, nextStep); - } - } - finally - { - if (jobCancelRegister != null) - { - jobCancelRegister?.Dispose(); - jobCancelRegister = null; - } + jobCancelRegister?.Dispose(); + jobCancelRegister = null; } } } @@ -401,125 +399,9 @@ namespace GitHub.Runner.Worker step.ExecutionContext.Debug($"Finishing: {step.DisplayName}"); } - private bool InitializeScope(IStep step, Dictionary scopeInputs) + private void CompleteStep(IStep step, TaskResult? result = null, string resultCode = null) { var executionContext = step.ExecutionContext; - var stepsContext = executionContext.StepsContext; - if (!string.IsNullOrEmpty(executionContext.ScopeName)) - { - // Gather uninitialized current and ancestor scopes - var scope = executionContext.Scopes[executionContext.ScopeName]; - var scopesToInitialize = default(Stack); - while (scope != null && !scopeInputs.ContainsKey(scope.Name)) - { - if (scopesToInitialize == null) - { - scopesToInitialize = new Stack(); - } - scopesToInitialize.Push(scope); - scope = string.IsNullOrEmpty(scope.ParentName) ? null : executionContext.Scopes[scope.ParentName]; - } - - // Initialize current and ancestor scopes - while (scopesToInitialize?.Count > 0) - { - scope = scopesToInitialize.Pop(); - executionContext.Debug($"Initializing scope '{scope.Name}'"); - executionContext.ExpressionValues["steps"] = stepsContext.GetScope(scope.ParentName); - // TODO: Fix this temporary workaround for Composite Actions - if (!executionContext.ExpressionValues.ContainsKey("inputs") && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TESTING_COMPOSITE_ACTIONS_ALPHA"))) - { - executionContext.ExpressionValues["inputs"] = !String.IsNullOrEmpty(scope.ParentName) ? scopeInputs[scope.ParentName] : null; - } - var templateEvaluator = executionContext.ToPipelineTemplateEvaluator(); - var inputs = default(DictionaryContextData); - try - { - inputs = templateEvaluator.EvaluateStepScopeInputs(scope.Inputs, executionContext.ExpressionValues, executionContext.ExpressionFunctions); - } - catch (Exception ex) - { - Trace.Info($"Caught exception from initialize scope '{scope.Name}'"); - Trace.Error(ex); - executionContext.Error(ex); - executionContext.Complete(TaskResult.Failed); - return false; - } - - scopeInputs[scope.Name] = inputs; - } - } - - // Setup expression values - var scopeName = executionContext.ScopeName; - executionContext.ExpressionValues["steps"] = stepsContext.GetScope(scopeName); - // TODO: Fix this temporary workaround for Composite Actions - if (!executionContext.ExpressionValues.ContainsKey("inputs") && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TESTING_COMPOSITE_ACTIONS_ALPHA"))) - { - executionContext.ExpressionValues["inputs"] = string.IsNullOrEmpty(scopeName) ? null : scopeInputs[scopeName]; - } - - return true; - } - - private void CompleteStep(IStep step, IStep nextStep, TaskResult? result = null, string resultCode = null) - { - var executionContext = step.ExecutionContext; - if (!string.IsNullOrEmpty(executionContext.ScopeName)) - { - // Gather current and ancestor scopes to finalize - var scope = executionContext.Scopes[executionContext.ScopeName]; - var scopesToFinalize = default(Queue); - var nextStepScopeName = nextStep?.ExecutionContext.ScopeName; - while (scope != null && - !string.Equals(nextStepScopeName, scope.Name, StringComparison.OrdinalIgnoreCase) && - !(nextStepScopeName ?? string.Empty).StartsWith($"{scope.Name}.", StringComparison.OrdinalIgnoreCase)) - { - if (scopesToFinalize == null) - { - scopesToFinalize = new Queue(); - } - scopesToFinalize.Enqueue(scope); - scope = string.IsNullOrEmpty(scope.ParentName) ? null : executionContext.Scopes[scope.ParentName]; - } - - // Finalize current and ancestor scopes - var stepsContext = step.ExecutionContext.StepsContext; - while (scopesToFinalize?.Count > 0) - { - scope = scopesToFinalize.Dequeue(); - executionContext.Debug($"Finalizing scope '{scope.Name}'"); - executionContext.ExpressionValues["steps"] = stepsContext.GetScope(scope.Name); - executionContext.ExpressionValues["inputs"] = null; - var templateEvaluator = executionContext.ToPipelineTemplateEvaluator(); - var outputs = default(DictionaryContextData); - try - { - outputs = templateEvaluator.EvaluateStepScopeOutputs(scope.Outputs, executionContext.ExpressionValues, executionContext.ExpressionFunctions); - } - catch (Exception ex) - { - Trace.Info($"Caught exception from finalize scope '{scope.Name}'"); - Trace.Error(ex); - executionContext.Error(ex); - executionContext.Complete(TaskResult.Failed); - return; - } - - if (outputs?.Count > 0) - { - var parentScopeName = scope.ParentName; - var contextName = scope.ContextName; - foreach (var pair in outputs) - { - var outputName = pair.Key; - var outputValue = pair.Value.ToString(); - stepsContext.SetOutput(parentScopeName, contextName, outputName, outputValue, out var reference); - executionContext.Debug($"{reference}='{outputValue}'"); - } - } - } - } executionContext.Complete(result, resultCode: resultCode); } diff --git a/src/Runner.Worker/action_yaml.json b/src/Runner.Worker/action_yaml.json index cb1d90b2e..82b24a695 100644 --- a/src/Runner.Worker/action_yaml.json +++ b/src/Runner.Worker/action_yaml.json @@ -7,7 +7,8 @@ "name": "string", "description": "string", "inputs": "inputs", - "runs": "runs" + "runs": "runs", + "outputs": "outputs" }, "loose-key-type": "non-empty-string", "loose-value-type": "any" @@ -28,6 +29,20 @@ "loose-value-type": "any" } }, + "outputs": { + "mapping": { + "loose-key-type": "non-empty-string", + "loose-value-type": "outputs-attributes" + } + }, + "outputs-attributes": { + "mapping": { + "properties": { + "description": "string", + "value": "output-value" + } + } + }, "runs": { "one-of": [ "container-runs", @@ -95,19 +110,13 @@ "composite-steps": { "context": [ "github", - "needs", "strategy", "matrix", - "secrets", "steps", "inputs", "job", "runner", "env", - "always(0,0)", - "failure(0,0)", - "cancelled(0,0)", - "success(0,0)", "hashFiles(1,255)" ], "sequence": { @@ -120,6 +129,19 @@ ], "string": {} }, + "output-value": { + "context": [ + "github", + "strategy", + "matrix", + "steps", + "inputs", + "job", + "runner", + "env" + ], + "string": {} + }, "input-default-context": { "context": [ "github",