Composite Run Steps Outputs (#568)

* Composite Action Run Steps

* Env Flow => Able to get env variables and overwrite current env variables => but it doesn't 'stick'

* clean up

* Clean up trace messages + add Trace debug in ActionManager

* Add debugging message

* Optimize runtime of code

* Change String to string

* Add comma to Composite

* Change JobSteps to a List, Change Register Step function name

* Add TODO, remove unn. content

* Remove unnecessary code

* Fix unit tests

* Fix env format

* Remove comment

* Remove TODO message for context

* Add verbose trace logs which are only viewable by devs

* Initial Start for FileTable stuff

* Progress towards passing FileTable or FileID or FileName

* Sort usings in Composite Action Handler

* Change 0 to location

* Update context variables in composite action yaml

* Add helpful error message for null steps

* Pass fileID to all children token of root action token

* Change confusing term context => templateContext, Eliminate _fileTable and only use ExecutionContext.FileTable + update this table when need be

* Remove unnessary FileID attribute from CompositeActionExecutionData

* Clean up file path for error message

* Remove todo

* Initial start/framework for output handling

* Outline different class vs Handler approach

* Remove InitializeScope

* Remove InitializeScope

* Fix Workflow Step Env overiding Parent Env

* First Approach for Attaching ID + Group ID to each Composite Action Step

* Add GroupID to the ActionDefinitionData

* starting foundation for handling clean up outputs step

* Pass outputs data to each composite action step to enable set-output functionality

* Create ScopeName for whole composite action.

This will enable us to add to the StepsContext[ScopeName] for the composite action which will allow us to use all these outputs in the cleanup step

* Hook up composite output step to handler => tmmrw implement composite output handler

* Add post composite action step to cleanup outputs => triggers composite output cleanup handler

* Fix Outputs Token handling start. Add individual step scope names.

* Set up Scope Name and Context Name naming system{

* Figured out how to pass Parent Execution Context to clean up step

* Figured out how to pass Parent Execution Context and scope names to
clean up step

* Add GetOutput function for StepsContext

* Generate child scope name correctly if parent scope name is null

* Simplify InitializeScope()

* Outputs are set correctly and able to get all final outputs in handler

* Parse through Action Outputs

* Fix null ScopeName + ContextName in CompositeOutputHandler

* Shift over handling of Action Outputs to output handler

* First attempt to fix null retrievals for output variables

* Basic Support for Outputs Done.

* Clean up pt.1

* Refactor outputs to avoid using Action Reference

* Clean up code

* Clean up part 2

* Add clarifying comments for the output handler

* Remove TODO

* Remove env in composite action scope

* Clean up

* Revert back

* revert back

* add back envToken

* Remove unnecessary code

* Add file length check

* Clean up

* Figure out how to handle set-env edge cases

* formatting

* fix unit tests

* Fix windows unit test syntax error

* Fix period

* Sanity check for fileTable add + remove unn. code

* revert back

* Add back line break

* Fix null errors

* Address situation if FileTable is null + add sanity check for adding file to fileTable

* add line

* Revert

* Fix unit tests to instantiate a FileTable

* Fix logic for trimming manifestfile path

* Add null check

* Revert

* Revert

* revert

* spacing

* Add filetable to testing file, remove ? since we know filetable should never be non null

* Fix Throw logic

* Clarify template outputs token

* Add another type support for outputs to avoid container unit tests errors

* Add mapping for parity

* Build support for new outputs format

* Refactor to avoid duplication of action yaml for workflow yaml

* Move SDK work in ActionManifestManager, Condense Code

* Defer runs evaluation till after for loop to ensure order doesn't matter

* Fix logic error in setting scope and context names

* Add Regex + Add Child Context name null resolution

* move private function to bottom of class
This commit is contained in:
Ethan Chiu
2020-07-13 17:23:19 -04:00
committed by GitHub
parent 6c3958f365
commit cb2b323781
8 changed files with 388 additions and 298 deletions

View File

@@ -398,8 +398,10 @@ namespace GitHub.Runner.Worker
else if (definition.Data.Execution.ExecutionType == ActionExecutionType.Composite && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TESTING_COMPOSITE_ACTIONS_ALPHA"))) else if (definition.Data.Execution.ExecutionType == ActionExecutionType.Composite && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TESTING_COMPOSITE_ACTIONS_ALPHA")))
{ {
var compositeAction = definition.Data.Execution as CompositeActionExecutionData; var compositeAction = definition.Data.Execution as CompositeActionExecutionData;
Trace.Info($"Load {compositeAction.Steps.Count} action steps."); Trace.Info($"Load {compositeAction.Steps?.Count ?? 0} action steps.");
Trace.Verbose($"Details: {StringUtil.ConvertToJson(compositeAction.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 else
{ {
@@ -1222,6 +1224,7 @@ namespace GitHub.Runner.Worker
public override bool HasPre => false; public override bool HasPre => false;
public override bool HasPost => false; public override bool HasPost => false;
public List<Pipelines.ActionStep> Steps { get; set; } public List<Pipelines.ActionStep> Steps { get; set; }
public MappingToken Outputs { get; set; }
} }
public abstract class ActionExecutionData public abstract class ActionExecutionData

View File

@@ -23,11 +23,15 @@ namespace GitHub.Runner.Worker
{ {
ActionDefinitionData Load(IExecutionContext executionContext, string manifestFile); ActionDefinitionData Load(IExecutionContext executionContext, string manifestFile);
DictionaryContextData EvaluateCompositeOutputs(IExecutionContext executionContext, TemplateToken token, IDictionary<string, PipelineContextData> extraExpressionValues);
List<string> EvaluateContainerArguments(IExecutionContext executionContext, SequenceToken token, IDictionary<string, PipelineContextData> extraExpressionValues); List<string> EvaluateContainerArguments(IExecutionContext executionContext, SequenceToken token, IDictionary<string, PipelineContextData> extraExpressionValues);
Dictionary<string, string> EvaluateContainerEnvironment(IExecutionContext executionContext, MappingToken token, IDictionary<string, PipelineContextData> extraExpressionValues); Dictionary<string, string> EvaluateContainerEnvironment(IExecutionContext executionContext, MappingToken token, IDictionary<string, PipelineContextData> extraExpressionValues);
string EvaluateDefaultInput(IExecutionContext executionContext, string inputName, TemplateToken token); string EvaluateDefaultInput(IExecutionContext executionContext, string inputName, TemplateToken token);
void SetAllCompositeOutputs(IExecutionContext parentExecutionContext, DictionaryContextData actionOutputs);
} }
public sealed class ActionManifestManager : RunnerService, IActionManifestManager public sealed class ActionManifestManager : RunnerService, IActionManifestManager
@@ -89,6 +93,9 @@ namespace GitHub.Runner.Worker
} }
var actionMapping = token.AssertMapping("action manifest root"); var actionMapping = token.AssertMapping("action manifest root");
var actionOutputs = default(MappingToken);
var actionRunValueToken = default(TemplateToken);
foreach (var actionPair in actionMapping) foreach (var actionPair in actionMapping)
{ {
var propertyName = actionPair.Key.AssertString($"action.yml property key"); var propertyName = actionPair.Key.AssertString($"action.yml property key");
@@ -99,6 +106,15 @@ namespace GitHub.Runner.Worker
actionDefinition.Name = actionPair.Value.AssertString("name").Value; actionDefinition.Name = actionPair.Value.AssertString("name").Value;
break; 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": case "description":
actionDefinition.Description = actionPair.Value.AssertString("description").Value; actionDefinition.Description = actionPair.Value.AssertString("description").Value;
break; break;
@@ -108,13 +124,21 @@ namespace GitHub.Runner.Worker
break; break;
case "runs": 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; break;
default: default:
Trace.Info($"Ignore action property {propertyName}."); Trace.Info($"Ignore action property {propertyName}.");
break; break;
} }
} }
// Evaluate Runs Last
if (actionRunValueToken != null)
{
actionDefinition.Execution = ConvertRuns(executionContext, templateContext, actionRunValueToken, actionOutputs);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -146,6 +170,61 @@ namespace GitHub.Runner.Worker
return actionDefinition; 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<string, PipelineContextData> 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<string> EvaluateContainerArguments( public List<string> EvaluateContainerArguments(
IExecutionContext executionContext, IExecutionContext executionContext,
SequenceToken token, SequenceToken token,
@@ -309,7 +388,8 @@ namespace GitHub.Runner.Worker
private ActionExecutionData ConvertRuns( private ActionExecutionData ConvertRuns(
IExecutionContext executionContext, IExecutionContext executionContext,
TemplateContext context, TemplateContext context,
TemplateToken inputsToken) TemplateToken inputsToken,
MappingToken outputs = null)
{ {
var runsMapping = inputsToken.AssertMapping("runs"); var runsMapping = inputsToken.AssertMapping("runs");
var usingToken = default(StringToken); var usingToken = default(StringToken);
@@ -439,6 +519,7 @@ namespace GitHub.Runner.Worker
return new CompositeActionExecutionData() return new CompositeActionExecutionData()
{ {
Steps = stepsLoaded, Steps = stepsLoaded,
Outputs = outputs
}; };
} }
} }

View File

@@ -6,6 +6,7 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web; using System.Web;
@@ -52,7 +53,6 @@ namespace GitHub.Runner.Worker
IDictionary<String, IDictionary<String, String>> JobDefaults { get; } IDictionary<String, IDictionary<String, String>> JobDefaults { get; }
Dictionary<string, VariableValue> JobOutputs { get; } Dictionary<string, VariableValue> JobOutputs { get; }
IDictionary<String, String> EnvironmentVariables { get; } IDictionary<String, String> EnvironmentVariables { get; }
IDictionary<String, ContextScope> Scopes { get; }
IList<String> FileTable { get; } IList<String> FileTable { get; }
StepsContext StepsContext { get; } StepsContext StepsContext { get; }
DictionaryContextData ExpressionValues { get; } DictionaryContextData ExpressionValues { get; }
@@ -70,6 +70,8 @@ namespace GitHub.Runner.Worker
bool EchoOnActionCommand { get; set; } bool EchoOnActionCommand { get; set; }
IExecutionContext FinalizeContext { get; set; }
// Initialize // Initialize
void InitializeJob(Pipelines.AgentJobRequestMessage message, CancellationToken token); void InitializeJob(Pipelines.AgentJobRequestMessage message, CancellationToken token);
void CancelToken(); void CancelToken();
@@ -105,7 +107,7 @@ namespace GitHub.Runner.Worker
// others // others
void ForceTaskComplete(); void ForceTaskComplete();
void RegisterPostJobStep(IStep step); void RegisterPostJobStep(IStep step);
void RegisterNestedStep(IStep step, DictionaryContextData inputsData, int location, Dictionary<string, string> envData); IStep RegisterNestedStep(IActionRunner step, DictionaryContextData inputsData, int location, Dictionary<string, string> envData, bool cleanUp = false);
} }
public sealed class ExecutionContext : RunnerService, IExecutionContext public sealed class ExecutionContext : RunnerService, IExecutionContext
@@ -120,6 +122,9 @@ namespace GitHub.Runner.Worker
private event OnMatcherChanged _onMatcherChanged; 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 IssueMatcherConfig[] _matchers;
private IPagingLogger _logger; private IPagingLogger _logger;
@@ -149,7 +154,6 @@ namespace GitHub.Runner.Worker
public IDictionary<String, IDictionary<String, String>> JobDefaults { get; private set; } public IDictionary<String, IDictionary<String, String>> JobDefaults { get; private set; }
public Dictionary<string, VariableValue> JobOutputs { get; private set; } public Dictionary<string, VariableValue> JobOutputs { get; private set; }
public IDictionary<String, String> EnvironmentVariables { get; private set; } public IDictionary<String, String> EnvironmentVariables { get; private set; }
public IDictionary<String, ContextScope> Scopes { get; private set; }
public IList<String> FileTable { get; private set; } public IList<String> FileTable { get; private set; }
public StepsContext StepsContext { get; private set; } public StepsContext StepsContext { get; private set; }
public DictionaryContextData ExpressionValues { get; } = new DictionaryContextData(); public DictionaryContextData ExpressionValues { get; } = new DictionaryContextData();
@@ -170,6 +174,8 @@ namespace GitHub.Runner.Worker
public bool EchoOnActionCommand { get; set; } public bool EchoOnActionCommand { get; set; }
public IExecutionContext FinalizeContext { get; set; }
public TaskResult? Result public TaskResult? Result
{ {
get get
@@ -270,17 +276,36 @@ namespace GitHub.Runner.Worker
/// Helper function used in CompositeActionHandler::RunAsync to /// 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. /// add a child node, aka a step, to the current job to the Root.JobSteps based on the location.
/// </summary> /// </summary>
public void RegisterNestedStep(IStep step, DictionaryContextData inputsData, int location, Dictionary<string, string> envData) public IStep RegisterNestedStep(
IActionRunner step,
DictionaryContextData inputsData,
int location,
Dictionary<string, string> 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 // 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(); 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:
// "__<GUID>"
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; 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. // 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 #if OS_WINDOWS
var envContext = new DictionaryContextData(); var envContext = new DictionaryContextData();
#else #else
@@ -293,6 +318,8 @@ namespace GitHub.Runner.Worker
step.ExecutionContext.ExpressionValues["env"] = envContext; step.ExecutionContext.ExpressionValues["env"] = envContext;
Root.JobSteps.Insert(location, step); Root.JobSteps.Insert(location, step);
return step;
} }
public IExecutionContext CreateChild(Guid recordId, string displayName, string refName, string scopeName, string contextName, Dictionary<string, string> intraActionState = null, int? recordOrder = null) public IExecutionContext CreateChild(Guid recordId, string displayName, string refName, string scopeName, string contextName, Dictionary<string, string> intraActionState = null, int? recordOrder = null)
@@ -317,7 +344,6 @@ namespace GitHub.Runner.Worker
} }
child.EnvironmentVariables = EnvironmentVariables; child.EnvironmentVariables = EnvironmentVariables;
child.JobDefaults = JobDefaults; child.JobDefaults = JobDefaults;
child.Scopes = Scopes;
child.FileTable = FileTable; child.FileTable = FileTable;
child.StepsContext = StepsContext; child.StepsContext = StepsContext;
foreach (var pair in ExpressionValues) foreach (var pair in ExpressionValues)
@@ -466,7 +492,8 @@ namespace GitHub.Runner.Worker
{ {
ArgUtil.NotNullOrEmpty(name, nameof(name)); 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; reference = null;
return; return;
@@ -633,16 +660,6 @@ namespace GitHub.Runner.Worker
// Steps context (StepsRunner manages adding the scoped steps context) // Steps context (StepsRunner manages adding the scoped steps context)
StepsContext = new StepsContext(); StepsContext = new StepsContext();
// Scopes
Scopes = new Dictionary<String, ContextScope>(StringComparer.OrdinalIgnoreCase);
if (message.Scopes?.Count > 0)
{
foreach (var scope in message.Scopes)
{
Scopes[scope.Name] = scope;
}
}
// File table // File table
FileTable = new List<String>(message.FileTable ?? new string[0]); FileTable = new List<String>(message.FileTable ?? new string[0]);

View File

@@ -47,6 +47,7 @@ namespace GitHub.Runner.Worker.Handlers
// Add each composite action step to the front of the queue // Add each composite action step to the front of the queue
int location = 0; int location = 0;
foreach (Pipelines.ActionStep aStep in actionSteps) foreach (Pipelines.ActionStep aStep in actionSteps)
{ {
// Ex: // Ex:
@@ -85,12 +86,35 @@ namespace GitHub.Runner.Worker.Handlers
actionRunner.Condition = aStep.Condition; actionRunner.Condition = aStep.Condition;
actionRunner.DisplayName = aStep.DisplayName; actionRunner.DisplayName = aStep.DisplayName;
ExecutionContext.RegisterNestedStep(actionRunner, inputsData, location, Environment); var step = ExecutionContext.RegisterNestedStep(actionRunner, inputsData, location, Environment);
InitializeScope(step);
location++; 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<IActionRunner>();
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; 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);
}
} }
} }

View File

@@ -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<IActionManifestManager>();
// Format ExpressionValues to Dictionary<string, PipelineContextData>
var evaluateContext = new Dictionary<string, PipelineContextData>(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;
}
}
}

View File

@@ -67,11 +67,19 @@ namespace GitHub.Runner.Worker.Handlers
(handler as IRunnerPluginHandler).Data = data as PluginActionExecutionData; (handler as IRunnerPluginHandler).Data = data as PluginActionExecutionData;
} }
else if (data.ExecutionType == ActionExecutionType.Composite) else if (data.ExecutionType == ActionExecutionType.Composite)
{
if (executionContext.FinalizeContext == null)
{ {
handler = HostContext.CreateService<ICompositeActionHandler>(); handler = HostContext.CreateService<ICompositeActionHandler>();
(handler as ICompositeActionHandler).Data = data as CompositeActionExecutionData; (handler as ICompositeActionHandler).Data = data as CompositeActionExecutionData;
} }
else else
{
handler = HostContext.CreateService<ICompositeActionOutputHandler>();
(handler as ICompositeActionOutputHandler).Data = data as CompositeActionExecutionData;
}
}
else
{ {
// This should never happen. // This should never happen.
throw new NotSupportedException(data.ExecutionType.ToString()); throw new NotSupportedException(data.ExecutionType.ToString());

View File

@@ -67,7 +67,6 @@ namespace GitHub.Runner.Worker
var step = jobContext.JobSteps[0]; var step = jobContext.JobSteps[0];
jobContext.JobSteps.RemoveAt(0); jobContext.JobSteps.RemoveAt(0);
var nextStep = jobContext.JobSteps.Count > 0 ? jobContext.JobSteps[0] : null;
Trace.Info($"Processing step: DisplayName='{step.DisplayName}'"); Trace.Info($"Processing step: DisplayName='{step.DisplayName}'");
ArgUtil.NotNull(step.ExecutionContext, nameof(step.ExecutionContext)); ArgUtil.NotNull(step.ExecutionContext, nameof(step.ExecutionContext));
@@ -83,9 +82,8 @@ namespace GitHub.Runner.Worker
step.ExecutionContext.ExpressionFunctions.Add(new FunctionInfo<SuccessFunction>(PipelineTemplateConstants.Success, 0, 0)); step.ExecutionContext.ExpressionFunctions.Add(new FunctionInfo<SuccessFunction>(PipelineTemplateConstants.Success, 0, 0));
step.ExecutionContext.ExpressionFunctions.Add(new FunctionInfo<HashFilesFunction>(PipelineTemplateConstants.HashFiles, 1, byte.MaxValue)); step.ExecutionContext.ExpressionFunctions.Add(new FunctionInfo<HashFilesFunction>(PipelineTemplateConstants.HashFiles, 1, byte.MaxValue));
// Initialize scope step.ExecutionContext.ExpressionValues["steps"] = step.ExecutionContext.StepsContext.GetScope(step.ExecutionContext.ScopeName);
if (InitializeScope(step, scopeInputs))
{
// Populate env context for each step // Populate env context for each step
Trace.Info("Initialize Env context for step"); Trace.Info("Initialize Env context for step");
#if OS_WINDOWS #if OS_WINDOWS
@@ -93,6 +91,7 @@ namespace GitHub.Runner.Worker
#else #else
var envContext = new CaseSensitiveDictionaryContextData(); var envContext = new CaseSensitiveDictionaryContextData();
#endif #endif
// Global env // Global env
foreach (var pair in step.ExecutionContext.EnvironmentVariables) foreach (var pair in step.ExecutionContext.EnvironmentVariables)
{ {
@@ -137,7 +136,7 @@ namespace GitHub.Runner.Worker
Trace.Info("Caught exception from expression for step.env"); Trace.Info("Caught exception from expression for step.env");
evaluateStepEnvFailed = true; evaluateStepEnvFailed = true;
step.ExecutionContext.Error(ex); step.ExecutionContext.Error(ex);
CompleteStep(step, nextStep, TaskResult.Failed); CompleteStep(step, TaskResult.Failed);
} }
} }
@@ -226,19 +225,19 @@ namespace GitHub.Runner.Worker
{ {
// Condition == false // Condition == false
Trace.Info("Skipping step due to condition evaluation."); Trace.Info("Skipping step due to condition evaluation.");
CompleteStep(step, nextStep, TaskResult.Skipped, resultCode: conditionTraceWriter.Trace); CompleteStep(step, TaskResult.Skipped, resultCode: conditionTraceWriter.Trace);
} }
else if (conditionEvaluateError != null) else if (conditionEvaluateError != null)
{ {
// fail the step since there is an evaluate error. // fail the step since there is an evaluate error.
step.ExecutionContext.Error(conditionEvaluateError); step.ExecutionContext.Error(conditionEvaluateError);
CompleteStep(step, nextStep, TaskResult.Failed); CompleteStep(step, TaskResult.Failed);
} }
else else
{ {
// Run the step. // Run the step.
await RunStepAsync(step, jobContext.CancellationToken); await RunStepAsync(step, jobContext.CancellationToken);
CompleteStep(step, nextStep); CompleteStep(step);
} }
} }
finally finally
@@ -250,7 +249,6 @@ namespace GitHub.Runner.Worker
} }
} }
} }
}
// Update the job result. // Update the job result.
if (step.ExecutionContext.Result == TaskResult.Failed) if (step.ExecutionContext.Result == TaskResult.Failed)
@@ -401,125 +399,9 @@ namespace GitHub.Runner.Worker
step.ExecutionContext.Debug($"Finishing: {step.DisplayName}"); step.ExecutionContext.Debug($"Finishing: {step.DisplayName}");
} }
private bool InitializeScope(IStep step, Dictionary<string, PipelineContextData> scopeInputs) private void CompleteStep(IStep step, TaskResult? result = null, string resultCode = null)
{ {
var executionContext = step.ExecutionContext; 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<ContextScope>);
while (scope != null && !scopeInputs.ContainsKey(scope.Name))
{
if (scopesToInitialize == null)
{
scopesToInitialize = new Stack<ContextScope>();
}
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<ContextScope>);
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<ContextScope>();
}
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); executionContext.Complete(result, resultCode: resultCode);
} }

View File

@@ -7,7 +7,8 @@
"name": "string", "name": "string",
"description": "string", "description": "string",
"inputs": "inputs", "inputs": "inputs",
"runs": "runs" "runs": "runs",
"outputs": "outputs"
}, },
"loose-key-type": "non-empty-string", "loose-key-type": "non-empty-string",
"loose-value-type": "any" "loose-value-type": "any"
@@ -28,6 +29,20 @@
"loose-value-type": "any" "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": { "runs": {
"one-of": [ "one-of": [
"container-runs", "container-runs",
@@ -95,19 +110,13 @@
"composite-steps": { "composite-steps": {
"context": [ "context": [
"github", "github",
"needs",
"strategy", "strategy",
"matrix", "matrix",
"secrets",
"steps", "steps",
"inputs", "inputs",
"job", "job",
"runner", "runner",
"env", "env",
"always(0,0)",
"failure(0,0)",
"cancelled(0,0)",
"success(0,0)",
"hashFiles(1,255)" "hashFiles(1,255)"
], ],
"sequence": { "sequence": {
@@ -120,6 +129,19 @@
], ],
"string": {} "string": {}
}, },
"output-value": {
"context": [
"github",
"strategy",
"matrix",
"steps",
"inputs",
"job",
"runner",
"env"
],
"string": {}
},
"input-default-context": { "input-default-context": {
"context": [ "context": [
"github", "github",