Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Composite Actions Support for Multiple Run Steps #549

Merged
merged 13 commits into from
Jun 23, 2020
19 changes: 19 additions & 0 deletions src/Runner.Worker/ActionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,11 @@ public Definition LoadAction(IExecutionContext executionContext, Pipelines.Actio
Trace.Info($"Action cleanup plugin: {plugin.PluginTypeName}.");
}
}
else if (definition.Data.Execution.ExecutionType == ActionExecutionType.Composite && !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("TESTING_COMPOSITE_ACTIONS_ALPHA")))
ethanchewy marked this conversation as resolved.
Show resolved Hide resolved
{
var compositeAction = definition.Data.Execution as CompositeActionExecutionData;
Trace.Info($"Action steps: {compositeAction.Steps}.");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will print Action steps: Object

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the json representation is desired (too noisy? esp with large inline scripts?) then see StringUtil or IOUtil (has function to convert to/from json)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TingluoHuang thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, compositeAction.Steps is of type List<Pipelines.ActionStep>. If we were to output this, what would be the best way to output the values in a json string format?

Right now, this Trace.info statement returns the object string representation: GitHub.DistributedTask.Pipelines.ActionStep

If we were to print out the steps, it might make more sense to print them out in the LoadCompositeSteps function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From @TingluoHuang (thanks Ting!!) :

Verbose will shows up only in dev mode (you build the runner from source code.)
Trace.Info($"Load {compositeAction.Steps.Count} action steps.");
Trace.Verbose($"Details: {StringUtil.ConvertToJson(compositeAction.Steps}");

So, I'll do that.

}
else
{
throw new NotSupportedException(definition.Data.Execution.ExecutionType.ToString());
Expand Down Expand Up @@ -1101,6 +1106,11 @@ private ActionContainer PrepareRepositoryActionAsync(IExecutionContext execution
Trace.Info($"Action plugin: {(actionDefinitionData.Execution as PluginActionExecutionData).Plugin}, no more preparation.");
return null;
}
else if (actionDefinitionData.Execution.ExecutionType == ActionExecutionType.Composite && !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("TESTING_COMPOSITE_ACTIONS_ALPHA")))
{
Trace.Info($"Action composite: {(actionDefinitionData.Execution as CompositeActionExecutionData).Steps}, no more preparation.");
return null;
}
else
{
throw new NotSupportedException(actionDefinitionData.Execution.ExecutionType.ToString());
Expand Down Expand Up @@ -1211,6 +1221,7 @@ public enum ActionExecutionType
NodeJS,
Plugin,
Script,
Composite
}

public sealed class ContainerActionExecutionData : ActionExecutionData
Expand Down Expand Up @@ -1267,6 +1278,14 @@ public sealed class ScriptActionExecutionData : ActionExecutionData
public override bool HasPost => false;
}

public sealed class CompositeActionExecutionData : ActionExecutionData
{
public override ActionExecutionType ExecutionType => ActionExecutionType.Composite;
public override bool HasPre => false;
public override bool HasPost => false;
public List<Pipelines.ActionStep> Steps { get; set; }
}

public abstract class ActionExecutionData
{
private string _initCondition = $"{Constants.Expressions.Always}()";
Expand Down
31 changes: 29 additions & 2 deletions src/Runner.Worker/ActionManifestManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using YamlDotNet.Core.Events;
using System.Globalization;
using System.Linq;
using Pipelines = GitHub.DistributedTask.Pipelines;

namespace GitHub.Runner.Worker
{
Expand Down Expand Up @@ -92,7 +93,7 @@ public ActionDefinitionData Load(IExecutionContext executionContext, string mani
break;

case "runs":
actionDefinition.Execution = ConvertRuns(context, actionPair.Value);
actionDefinition.Execution = ConvertRuns(executionContext, context, actionPair.Value);
break;
default:
Trace.Info($"Ignore action property {propertyName}.");
Expand Down Expand Up @@ -284,7 +285,7 @@ private TemplateContext CreateContext(
// Add the file table
if (_fileTable?.Count > 0)
{
for (var i = 0 ; i < _fileTable.Count ; i++)
for (var i = 0; i < _fileTable.Count; i++)
{
result.GetFileId(_fileTable[i]);
}
Expand All @@ -294,6 +295,7 @@ private TemplateContext CreateContext(
}

private ActionExecutionData ConvertRuns(
IExecutionContext executionContext,
TemplateContext context,
TemplateToken inputsToken)
{
Expand All @@ -311,6 +313,8 @@ private ActionExecutionData ConvertRuns(
var postToken = default(StringToken);
var postEntrypointToken = default(StringToken);
var postIfToken = default(StringToken);
var stepsLoaded = default(List<Pipelines.ActionStep>);

foreach (var run in runsMapping)
{
var runsKey = run.Key.AssertString("runs key").Value;
Expand Down Expand Up @@ -355,6 +359,15 @@ private ActionExecutionData ConvertRuns(
case "pre-if":
preIfToken = run.Value.AssertString("pre-if");
break;
case "steps":
if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("TESTING_COMPOSITE_ACTIONS_ALPHA")))
{
var steps = run.Value.AssertSequence("steps");
var evaluator = executionContext.ToPipelineTemplateEvaluator();
stepsLoaded = evaluator.LoadCompositeSteps(steps);
break;
}
throw new Exception("You aren't supposed to be using Composite Actions yet!");
default:
Trace.Info($"Ignore run property {runsKey}.");
break;
Expand Down Expand Up @@ -402,6 +415,20 @@ private ActionExecutionData ConvertRuns(
};
}
}
else if (string.Equals(usingToken.Value, "composite", StringComparison.OrdinalIgnoreCase) && !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("TESTING_COMPOSITE_ACTIONS_ALPHA")))
{
if (stepsLoaded == null)
{
throw new ArgumentNullException($"No steps provided.");
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this error give the user enough context to know where the problem is? Like does it convey that an error happened when loading the path-to-action.yml

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess we would have the same problem with the other place too (in this file)

Copy link
Contributor Author

@ethanchewy ethanchewy Jun 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this error makes sense and is appropriate.

At the top of ConvertRuns:
var stepsLoaded = default(List<Pipelines.ActionStep>);

Then, we check in the action.yml file to see if there is any steps token:

case "steps":
                        if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TESTING_COMPOSITE_ACTIONS_ALPHA")))
                        {
                            var steps = run.Value.AssertSequence("steps");
                            var evaluator = executionContext.ToPipelineTemplateEvaluator();
                            stepsLoaded = evaluator.LoadCompositeSteps(steps);
                            break;
                        }
                        throw new Exception("You aren't supposed to be using Composite Actions yet!");

If there are any issues with loading the steps, there would be an error reported in the LoadCompositeSteps() or LoadStep() function in the pipeline template evaulator. If the steps attribute is empty, there would be no error and LoadCompositeSteps would return the default value of List which is equivalent to null.

Thus, if stepsLoaded is null, it must be because there was no steps attribute written in the action.yml by the user.

Thus, I believe throwing this message is appropriate.

Copy link
Contributor Author

@ethanchewy ethanchewy Jun 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Add a more helpful error message + including file name, etc. to show user that it's because of their yaml file

(Just add TODO for now)

}
else
{
return new CompositeActionExecutionData()
{
Steps = stepsLoaded,
};
}
}
else
{
throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker' or 'node12' instead.");
Expand Down
61 changes: 60 additions & 1 deletion src/Runner.Worker/ExecutionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ public interface IExecutionContext : IRunnerService
// others
void ForceTaskComplete();
void RegisterPostJobStep(IStep step);
IStep RegisterCompositeStep(IStep step, DictionaryContextData inputsData);
ethanchewy marked this conversation as resolved.
Show resolved Hide resolved
void EnqueueAllCompositeSteps(Queue<IStep> steps);
ethanchewy marked this conversation as resolved.
Show resolved Hide resolved
}

public sealed class ExecutionContext : RunnerService, IExecutionContext
Expand Down Expand Up @@ -169,7 +171,6 @@ public sealed class ExecutionContext : RunnerService, IExecutionContext

public bool EchoOnActionCommand { get; set; }


public TaskResult? Result
{
get
Expand Down Expand Up @@ -266,6 +267,64 @@ public void RegisterPostJobStep(IStep step)
Root.PostJobSteps.Push(step);
}

/*
ethanchewy marked this conversation as resolved.
Show resolved Hide resolved
RegisterCompositeStep is a helper function used in CompositeActionHandler::RunAsync to
add a child node, aka a step, to the current job to the front of the queue for processing.
*/
public IStep RegisterCompositeStep(IStep step, DictionaryContextData inputsData)
{
// ~Brute Force Method~
ethanchewy marked this conversation as resolved.
Show resolved Hide resolved
// There is no way to put this current job in front of the queue in < O(n) time where n = # of elements in JobSteps
// Everytime we add a new step, you could requeue every item to put those steps from that stack in JobSteps which
// would result in O(n) for each time we add a composite action step where n = number of jobSteps which would compound
// to O(n*m) where m = number of composite steps
// var temp = Root.JobSteps.ToArray();
// Root.JobSteps.Clear();
// Root.JobSteps.Enqueue(step);
// foreach(var s in temp)
// Root.JobSteps.Enqueue(s);

// ~Optimized Method~
// Alterative solution: We add to another temp Queue
// After we add all the transformed composite steps to this temp queue, we requeue the whole JobSteps accordingly in EnqueueAllCompositeSteps()
// where the queued composite steps are at the front of the JobSteps Queue and the rest of the jobs maintain its order and are
// placed after the queued composite steps
// This will take only O(n+m) time which would be just as efficient if we refactored the code of JobSteps to a PriorityQueue
// This temp Queue is created in the CompositeActionHandler.
var newGuid = Guid.NewGuid();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm worried a little bit because we're not adding the contexts from the job message, like we normally do when creating steps.

I know this is what Post does, but wouldnt surprise me if there is a bug there too.

It may be safe, since we're copying the root node (maybe job message contexts copied there first so safe?)

Let's add a TODO comment in the code like "// TODO: confirm whether not copying message contexts is safe".

I can help look into this more. It's tedious because values are delay expanded sometimes (sometimes evaluated early, like DisplayName). I wish the code were designed simpler... hmm thinking about alternatives.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added TODO. Leaving open for visibility.

Copy link
Contributor Author

@ethanchewy ethanchewy Jun 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After more testing for the env flow, I realized that the display name does not evaluate the env variables. This has to do with not passing the context. See: #557 (comment)
Ex: https://github.com/ethanchewy/testing-actions/runs/795652488?check_suite_focus=true
Building in support will occur in the env flow PR: #557

Never mind, in both the composite action and workflow steps, the environment contexts are evaluated correctly: https://github.com/ethanchewy/testing-actions/actions/runs/143774560

Still investigating but for now I think the contexts are handled correctly for the composite action steps.

Copy link
Contributor Author

@ethanchewy ethanchewy Jun 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tl;dr The context is exposed correctly in env flow PR and solves this concern

In the env flow PR, I handle the contexts for each correctly: https://github.com/actions/runner/pull/557/files#diff-e3d54b1d0cbdce4d358272df29857cf2R225 by setting the step.EnvironmentVariables to the environment variables.

Demo: https://github.com/ethanchewy/testing-actions/actions/runs/143774560

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, since we add each of our composite steps to JobSteps, eventually each of the steps will be run via the ScriptHandler.

In the ScriptHandler, it exposes the context to the step/job: https://github.com/actions/runner/blob/master/src/Runner.Worker/Handlers/ScriptHandler.cs#L248-L258

step.ExecutionContext = Root.CreateChild(newGuid, step.DisplayName, newGuid.ToString("N"), null, null);
step.ExecutionContext.ExpressionValues["inputs"] = inputsData;
return step;
}

// Add Composite Steps first and then requeue the rest of the job steps.
public void EnqueueAllCompositeSteps(Queue<IStep> steps)
{
// TODO: For UI purposes, look at figuring out how to condense steps in one node
// maybe use "this" instead of "Root"?
if (Root.JobSteps != null)
{
var temp = Root.JobSteps.ToArray();
Root.JobSteps.Clear();
foreach (var cs in steps)
{
Root.JobSteps.Enqueue(cs);
}
foreach (var s in temp)
{
Root.JobSteps.Enqueue(s);
}
}
else
{
Root.JobSteps = new Queue<IStep>();
foreach (var cs in steps)
{
Root.JobSteps.Enqueue(cs);
}
}
}

public IExecutionContext CreateChild(Guid recordId, string displayName, string refName, string scopeName, string contextName, Dictionary<string, string> intraActionState = null, int? recordOrder = null)
{
Trace.Entering();
Expand Down
97 changes: 97 additions & 0 deletions src/Runner.Worker/Handlers/CompositeActionHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.IO;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sort

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

system first

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

named at the bottom

using System.Text;
using System.Threading.Tasks;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using GitHub.DistributedTask.WebApi;
using Pipelines = GitHub.DistributedTask.Pipelines;
using System;
using System.Linq;
using GitHub.DistributedTask.ObjectTemplating.Tokens;
using System.Collections.Generic;
using GitHub.DistributedTask.Pipelines.ContextData;

namespace GitHub.Runner.Worker.Handlers
{
[ServiceLocator(Default = typeof(CompositeActionHandler))]
public interface ICompositeActionHandler : IHandler
{
CompositeActionExecutionData Data { get; set; }
}
public sealed class CompositeActionHandler : Handler, ICompositeActionHandler
{
public CompositeActionExecutionData Data { get; set; }

public Task RunAsync(ActionRunStage stage)
{
// Validate args.
Trace.Entering();
ArgUtil.NotNull(ExecutionContext, nameof(ExecutionContext));
ArgUtil.NotNull(Inputs, nameof(Inputs));

var githubContext = ExecutionContext.ExpressionValues["github"] as GitHubContext;
ArgUtil.NotNull(githubContext, nameof(githubContext));

var tempDirectory = HostContext.GetDirectory(WellKnownDirectory.Temp);

// Resolve action steps
var actionSteps = Data.Steps;

// Create Context Data to reuse for each composite action step
var inputsData = new DictionaryContextData();
foreach (var i in Inputs)
{
inputsData[i.Key] = new StringContextData(i.Value);
}

// Add each composite action step to the front of the queue
var compositeActionSteps = new Queue<IStep>();
foreach (Pipelines.ActionStep aStep in actionSteps)
{
// Ex:
// runs:
// using: "composite"
// steps:
// - uses: example/test-composite@v2 (a)
// - run echo hello world (b)
// - run echo hello world 2 (c)
//
// ethanchewy/test-composite/action.yaml
// runs:
// using: "composite"
// steps:
// - run echo hello world 3 (d)
// - run echo hello world 4 (e)
//
// Stack (LIFO) [Bottom => Middle => Top]:
// | a |
// | a | => | d |
// (Run step d)
// | a |
// | a | => | e |
// (Run step e)
// | a |
// (Run step a)
// | b |
// (Run step b)
// | c |
// (Run step c)
// Done.

var actionRunner = HostContext.CreateService<IActionRunner>();
actionRunner.Action = aStep;
actionRunner.Stage = stage;
actionRunner.Condition = aStep.Condition;
actionRunner.DisplayName = aStep.DisplayName;
// TODO: Do we need to add any context data from the job message?
// (See JobExtension.cs ~line 236)

compositeActionSteps.Enqueue(ExecutionContext.RegisterCompositeStep(actionRunner, inputsData));
}
ExecutionContext.EnqueueAllCompositeSteps(compositeActionSteps);

return Task.CompletedTask;
}

}
}
8 changes: 8 additions & 0 deletions src/Runner.Worker/Handlers/HandlerFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ public IHandler Create(
handler = HostContext.CreateService<IRunnerPluginHandler>();
(handler as IRunnerPluginHandler).Data = data as PluginActionExecutionData;
}
else if (data.ExecutionType == ActionExecutionType.Composite)
{
// TODO
ethanchewy marked this conversation as resolved.
Show resolved Hide resolved
// Runner plugin
handler = HostContext.CreateService<ICompositeActionHandler>();
// handler = CompositeHandler;
ethanchewy marked this conversation as resolved.
Show resolved Hide resolved
(handler as ICompositeActionHandler).Data = data as CompositeActionExecutionData;
}
else
{
// This should never happen.
Expand Down
17 changes: 15 additions & 2 deletions src/Runner.Worker/StepsRunner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,11 @@ public async Task RunAsync(IExecutionContext jobContext)

var step = jobContext.JobSteps.Dequeue();
var nextStep = jobContext.JobSteps.Count > 0 ? jobContext.JobSteps.Peek() : null;
// TODO: Fix this temporary workaround for Composite Actions
if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("TESTING_COMPOSITE_ACTIONS_ALPHA")))
ethanchewy marked this conversation as resolved.
Show resolved Hide resolved
{
nextStep = null;
}

Trace.Info($"Processing step: DisplayName='{step.DisplayName}'");
ArgUtil.NotNull(step.ExecutionContext, nameof(step.ExecutionContext));
Expand Down Expand Up @@ -409,7 +414,11 @@ private bool InitializeScope(IStep step, Dictionary<string, PipelineContextData>
scope = scopesToInitialize.Pop();
executionContext.Debug($"Initializing scope '{scope.Name}'");
executionContext.ExpressionValues["steps"] = stepsContext.GetScope(scope.ParentName);
executionContext.ExpressionValues["inputs"] = !String.IsNullOrEmpty(scope.ParentName) ? scopeInputs[scope.ParentName] : null;
// 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
Expand All @@ -432,7 +441,11 @@ private bool InitializeScope(IStep step, Dictionary<string, PipelineContextData>
// Setup expression values
var scopeName = executionContext.ScopeName;
executionContext.ExpressionValues["steps"] = stepsContext.GetScope(scopeName);
executionContext.ExpressionValues["inputs"] = string.IsNullOrEmpty(scopeName) ? null : scopeInputs[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;
}
Expand Down
Loading