Compare commits

...

12 Commits

Author SHA1 Message Date
Ethan Chiu
b2c1ea71f9 Add feature flag 2020-06-15 12:03:00 -04:00
Ethan Chiu
a0d93904ee Fix error + add diagnostics 2020-06-15 11:10:36 -04:00
Ethan Chiu
e654a3e885 Fix templating + assertion on template validation 2020-06-15 10:09:05 -04:00
Ethan Chiu
85796d0011 Resolve build errors 2020-06-15 09:12:54 -04:00
Ethan Chiu
885a128fa5 prep for first test build 2020-06-12 15:34:54 -04:00
Ethan Chiu
46f7da1628 Thoughts on next steps 2020-06-12 15:28:22 -04:00
Ethan Chiu
7abdde0ad9 Step => ActionStep parity 2020-06-11 17:55:13 -04:00
Ethan Chiu
e601a3f4be Add necessary files + add functionality to convert to ActionStep object 2020-06-11 17:54:24 -04:00
Ethan Chiu
4cb06b9edb Add comments of alternative mapping solution 2020-06-11 10:46:36 -04:00
Ethan Chiu
65287fb0f0 Add comments + fix action_yaml schema 2020-06-11 10:37:29 -04:00
Ethan Chiu
3fed00814d rename 2020-06-10 18:01:42 -04:00
Ethan Chiu
e2bb8d9e24 First attempt at creating baseline code for composite actions 2020-06-10 17:50:14 -04:00
11 changed files with 928 additions and 13 deletions

View File

@@ -37,6 +37,8 @@ namespace GitHub.Runner.Listener
{
try
{
_term.WriteLine("Runner Repo TESTING VERSION OF RUNNER...");
Trace.Info("Hey I'm in the log file yay!");
VssUtil.InitializeVssClientSettings(HostContext.UserAgents, HostContext.WebProxy);
_inConfigStage = true;

View File

@@ -395,6 +395,9 @@ namespace GitHub.Runner.Worker
Trace.Info($"Action cleanup plugin: {plugin.PluginTypeName}.");
}
}
else if (definition.Data.Execution.ExecutionType == ActionExecutionType.Composite && !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("TESTING_COMPOSITE_ACTIONS_ALPHA"))) {
// Don't do anything for now
}
else
{
throw new NotSupportedException(definition.Data.Execution.ExecutionType.ToString());
@@ -1101,6 +1104,12 @@ namespace GitHub.Runner.Worker
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).Unknown}, no more preparation.");
return null;
}
else
{
throw new NotSupportedException(actionDefinitionData.Execution.ExecutionType.ToString());
@@ -1211,6 +1220,7 @@ namespace GitHub.Runner.Worker
NodeJS,
Plugin,
Script,
Composite
}
public sealed class ContainerActionExecutionData : ActionExecutionData
@@ -1267,6 +1277,25 @@ namespace GitHub.Runner.Worker
public override bool HasPost => false;
}
public sealed class CompositeActionExecutionData : ActionExecutionData
{
// Do I actually need this??
// Pre => pre execution for checking steps before, ibid for post
// Look at Script for dummy example
public override ActionExecutionType ExecutionType => ActionExecutionType.Composite;
public override bool HasPre => false;
public override bool HasPost => false;
public List<Pipelines.ActionStep> Steps {get; set;}
// public string Script { get; set; }
// public string Pre { get; set; }
// public string Post { get; set; }
}
public abstract class ActionExecutionData
{
private string _initCondition = $"{Constants.Expressions.Always}()";

View File

@@ -14,6 +14,7 @@ using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using System.Globalization;
using System.Linq;
using GitHub.DistributedTask.Pipelines;
namespace GitHub.Runner.Worker
{
@@ -92,7 +93,7 @@ namespace GitHub.Runner.Worker
break;
case "runs":
actionDefinition.Execution = ConvertRuns(context, actionPair.Value);
actionDefinition.Execution = ConvertRuns(executionContext, context, actionPair.Value);
break;
default:
Trace.Info($"Ignore action property {propertyName}.");
@@ -294,8 +295,10 @@ namespace GitHub.Runner.Worker
}
private ActionExecutionData ConvertRuns(
IExecutionContext executionContext,
TemplateContext context,
TemplateToken inputsToken)
TemplateToken inputsToken
)
{
var runsMapping = inputsToken.AssertMapping("runs");
var usingToken = default(StringToken);
@@ -311,6 +314,15 @@ namespace GitHub.Runner.Worker
var postToken = default(StringToken);
var postEntrypointToken = default(StringToken);
var postIfToken = default(StringToken);
// TODO: How do I represent stepsToken as a list of steps?
// Well for default value, we can set it to this?
// var stepsToken = runsMapping.AssertMapping("steps");
// Actually, not sure, let's just set it to MappingToken since AssertMapping("steps")
// returns a MappingToken
// var stepsToken = default(SequenceToken);
var stepsLoaded = default(List<ActionStep>);
// It should be a array (aka sequence)
foreach (var run in runsMapping)
{
var runsKey = run.Key.AssertString("runs key").Value;
@@ -355,6 +367,22 @@ namespace GitHub.Runner.Worker
case "pre-if":
preIfToken = run.Value.AssertString("pre-if");
break;
case "steps":
// stepsToken = run.Value.AssertMapping("steps");
// Maybe insert a for loop here instead since MappingToken is not supposed to be used in HandlerFactory.cs
// Just support 1 layer of steps w/ just run
var steps = run.Value.AssertSequence("steps");
// foreach (var s in steps) {
// // Create list of steps
// loadS
// }
// foreach (var run in runsMapping)
// stepsToken = List
// Call load steps here
// var test = new PipelineTemplateEvaluator();
var evaluator = executionContext.ToPipelineTemplateEvaluator();
stepsLoaded = evaluator.LoadSteps(steps, null, null);
break;
default:
Trace.Info($"Ignore run property {runsKey}.");
break;
@@ -402,6 +430,24 @@ namespace GitHub.Runner.Worker
};
}
}
// TODO: add composite stuff here
else if (string.Equals(usingToken.Value, "composite", StringComparison.OrdinalIgnoreCase))
{
// if (stepsToken.Count <= 0)
// {
// throw new ArgumentNullException($"No steps provided.");
// }
if (stepsLoaded == null) {
throw new ArgumentNullException($"No steps provided.");
}
else
{
return new CompositeActionExecutionData()
{
Steps = stepsLoaded
};
}
}
else
{
throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker' or 'node12' instead.");

View File

@@ -0,0 +1,399 @@
using System.IO;
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; }
}
// TODO: IMPLEMENT LOGIC FOR HANDLER CODE
public sealed class CompositeActionHandler : Handler, ICompositeActionHandler
{
public CompositeActionExecutionData Data { get; set; }
// TODO: Implement PrintActionDetails()
public override void PrintActionDetails(ActionRunStage stage)
{
// Just keep as same as ScriptHandler.cs for now
var target = Data.Steps;
var runStepInputs = target[0].Inputs;
var templateEvaluator = ExecutionContext.ToPipelineTemplateEvaluator();
var inputs = templateEvaluator.EvaluateStepInputs(runStepInputs, ExecutionContext.ExpressionValues, ExecutionContext.ExpressionFunctions);
var taskManager = HostContext.GetService<IActionManager>();
var userInputs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var runValue = "";
foreach (KeyValuePair<string, string> input in inputs)
{
userInputs.Add(input.Key);
userInputs.Add(input.Value);
if (input.Key.Equals("run"))
{
runValue = input.Value;
}
}
var contents = runValue ?? string.Empty;
if (Action.Type == Pipelines.ActionSourceType.Repository)
{
var firstLine = contents.TrimStart(' ', '\t', '\r', '\n');
var firstNewLine = firstLine.IndexOfAny(new[] { '\r', '\n' });
if (firstNewLine >= 0)
{
firstLine = firstLine.Substring(0, firstNewLine);
}
ExecutionContext.Output($"##[group]Run {firstLine}");
}
else
{
throw new InvalidOperationException($"Invalid action type {Action.Type} for {nameof(ScriptHandler)}");
}
var multiLines = contents.Replace("\r\n", "\n").TrimEnd('\n').Split('\n');
foreach (var line in multiLines)
{
// Bright Cyan color
ExecutionContext.Output($"\x1b[36;1m{line}\x1b[0m");
}
string argFormat;
string shellCommand;
string shellCommandPath = null;
bool validateShellOnHost = !(StepHost is ContainerStepHost);
string prependPath = string.Join(Path.PathSeparator.ToString(), ExecutionContext.PrependPath.Reverse<string>());
string shell = null;
if (!Inputs.TryGetValue("shell", out shell) || string.IsNullOrEmpty(shell))
{
// TODO: figure out how defaults interact with template later
// for now, we won't check job.defaults if we are inside a template.
if (string.IsNullOrEmpty(ExecutionContext.ScopeName) && ExecutionContext.JobDefaults.TryGetValue("run", out var runDefaults))
{
runDefaults.TryGetValue("shell", out shell);
}
}
if (string.IsNullOrEmpty(shell))
{
#if OS_WINDOWS
shellCommand = "pwsh";
if (validateShellOnHost)
{
shellCommandPath = WhichUtil.Which(shellCommand, require: false, Trace, prependPath);
if (string.IsNullOrEmpty(shellCommandPath))
{
shellCommand = "powershell";
Trace.Info($"Defaulting to {shellCommand}");
shellCommandPath = WhichUtil.Which(shellCommand, require: true, Trace, prependPath);
}
}
#else
shellCommand = "sh";
if (validateShellOnHost)
{
shellCommandPath = WhichUtil.Which("bash", false, Trace, prependPath) ?? WhichUtil.Which("sh", true, Trace, prependPath);
}
#endif
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
}
else
{
var parsed = ScriptHandlerHelpers.ParseShellOptionString(shell);
shellCommand = parsed.shellCommand;
if (validateShellOnHost)
{
shellCommandPath = WhichUtil.Which(parsed.shellCommand, true, Trace, prependPath);
}
argFormat = $"{parsed.shellArgs}".TrimStart();
if (string.IsNullOrEmpty(argFormat))
{
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
}
}
if (!string.IsNullOrEmpty(shellCommandPath))
{
ExecutionContext.Output($"shell: {shellCommandPath} {argFormat}");
}
else
{
ExecutionContext.Output($"shell: {shellCommand} {argFormat}");
}
if (this.Environment?.Count > 0)
{
ExecutionContext.Output("env:");
foreach (var env in this.Environment)
{
ExecutionContext.Output($" {env.Key}: {env.Value}");
}
}
ExecutionContext.Output("##[endgroup]");
}
public async Task RunAsync(ActionRunStage stage)
{
// DELETE LATER
// await Task.Yield();
// Basically, the only difference from ScriptHandler.cs is that "contents" is not just each step under "run: "
// It might make more sense to:
// 1) Abstract the core functionality of the ScriptHandler.cs that we need for BOTH CompositeActionHandler.cs and ScriptHandler.cs
// 2) Call those functions in both handlers
// * There is already a file called ScriptHandlerHelpers.cs that might be a good location to add more functions.
// Copied from ScriptHandler.cs
// 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 steps
var target = Data.Steps;
if (target == null) {
Trace.Error("Data.Steps in CompositeActionHandler is null");
} else {
Trace.Info($"Data Steps Value for Composite Actions is: {target}.");
}
// For now, just assume it is 1 Run step
// We will adapt this in the future.
// Copied from ActionRunner.cs RunAsync() function => Maybe we don't need a handler and need to avoid this preplicatoin in the future?
var runStepInputs = target[0].Inputs;
if (runStepInputs == null) {
Trace.Error("runStepInputs in CompositeActionHandler is null");
} else {
Trace.Info($"runStepInputs Value for Composite Actions is: {runStepInputs}.");
}
var templateEvaluator = ExecutionContext.ToPipelineTemplateEvaluator();
var inputs = templateEvaluator.EvaluateStepInputs(runStepInputs, ExecutionContext.ExpressionValues, ExecutionContext.ExpressionFunctions);
var taskManager = HostContext.GetService<IActionManager>();
var userInputs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var runValue = "";
foreach (KeyValuePair<string, string> input in inputs)
{
userInputs.Add(input.Key);
userInputs.Add(input.Value);
Trace.Info($"Composite Action Handler. Key: {input.Key} Value: {input.Value}");
// Why is the key should not be "run" => because the "run" keyword is recorgnized as something that will be a "script"?
// Or should we create another key that delineates between "script" and "run"?
// ^ Perhaps we can explore this in the next version with the changes the to action.yaml template
if (input.Key.Equals("script"))
{
runValue = input.Value;
}
// 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));
// }
// Get the run bash value that we want to run
// In the future, we would apply and validate the template => maybe using the manifest manager to recursively load the json schema.
}
Trace.Info($"Run Value for Composite Actions is: {runValue}.");
// Let's think about validating inputs later
// Validate inputs only for actions with action.yml
// 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)}']");
// }
//TODO:
// 6/11/20 EOD thoughts
// => What functions do I need to use from ScriptHandlerHelpers.cs?
// Do I need to Reverse the string for prepending the path??
// ^ why or why not?
// How do I process the Inputs?
// => It's a TemplateToken
// How do I incorporate Async? we call the StepHost for Async when you call ExecuteAsync()
// Detect operating system for fileName + arguments
var contents = runValue ?? string.Empty;
string workingDirectory = null;
if (!Inputs.TryGetValue("workingDirectory", out workingDirectory))
{
// TODO: figure out how defaults interact with template later
// for now, we won't check job.defaults if we are inside a template.
if (string.IsNullOrEmpty(ExecutionContext.ScopeName) && ExecutionContext.JobDefaults.TryGetValue("run", out var runDefaults))
{
if (runDefaults.TryGetValue("working-directory", out workingDirectory))
{
ExecutionContext.Debug("Overwrite 'working-directory' base on job defaults.");
}
}
}
var workspaceDir = githubContext["workspace"] as StringContextData;
workingDirectory = Path.Combine(workspaceDir, workingDirectory ?? string.Empty);
string shell = null;
if (!Inputs.TryGetValue("shell", out shell) || string.IsNullOrEmpty(shell))
{
// TODO: figure out how defaults interact with template later
// for now, we won't check job.defaults if we are inside a template.
if (string.IsNullOrEmpty(ExecutionContext.ScopeName) && ExecutionContext.JobDefaults.TryGetValue("run", out var runDefaults))
{
if (runDefaults.TryGetValue("shell", out shell))
{
ExecutionContext.Debug("Overwrite 'shell' base on job defaults.");
}
}
}
var isContainerStepHost = StepHost is ContainerStepHost;
string prependPath = string.Join(Path.PathSeparator.ToString(), ExecutionContext.PrependPath.Reverse<string>());
string commandPath, argFormat, shellCommand;
if (string.IsNullOrEmpty(shell))
{
#if OS_WINDOWS
shellCommand = "pwsh";
commandPath = WhichUtil.Which(shellCommand, require: false, Trace, prependPath);
if (string.IsNullOrEmpty(commandPath))
{
shellCommand = "powershell";
Trace.Info($"Defaulting to {shellCommand}");
commandPath = WhichUtil.Which(shellCommand, require: true, Trace, prependPath);
}
ArgUtil.NotNullOrEmpty(commandPath, "Default Shell");
#else
shellCommand = "sh";
commandPath = WhichUtil.Which("bash", false, Trace, prependPath) ?? WhichUtil.Which("sh", true, Trace, prependPath);
#endif
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
}
else
{
var parsed = ScriptHandlerHelpers.ParseShellOptionString(shell);
shellCommand = parsed.shellCommand;
// For non-ContainerStepHost, the command must be located on the host by Which
commandPath = WhichUtil.Which(parsed.shellCommand, !isContainerStepHost, Trace, prependPath);
argFormat = $"{parsed.shellArgs}".TrimStart();
if (string.IsNullOrEmpty(argFormat))
{
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
}
}
// No arg format was given, shell must be a built-in
if (string.IsNullOrEmpty(argFormat) || !argFormat.Contains("{0}"))
{
throw new ArgumentException("Invalid shell option. Shell must be a valid built-in (bash, sh, cmd, powershell, pwsh) or a format string containing '{0}'");
}
// We do not not the full path until we know what shell is being used, so that we can determine the file extension
var scriptFilePath = Path.Combine(tempDirectory, $"{Guid.NewGuid()}{ScriptHandlerHelpers.GetScriptFileExtension(shellCommand)}");
var resolvedScriptPath = $"{StepHost.ResolvePathForStepHost(scriptFilePath).Replace("\"", "\\\"")}";
// Format arg string with script path
var arguments = string.Format(argFormat, resolvedScriptPath);
// Fix up and write the script
contents = ScriptHandlerHelpers.FixUpScriptContents(shellCommand, contents);
#if OS_WINDOWS
// Normalize Windows line endings
contents = contents.Replace("\r\n", "\n").Replace("\n", "\r\n");
var encoding = ExecutionContext.Variables.Retain_Default_Encoding && Console.InputEncoding.CodePage != 65001
? Console.InputEncoding
: new UTF8Encoding(false);
#else
// Don't add a BOM. It causes the script to fail on some operating systems (e.g. on Ubuntu 14).
var encoding = new UTF8Encoding(false);
#endif
// Script is written to local path (ie host) but executed relative to the StepHost, which may be a container
File.WriteAllText(scriptFilePath, contents, encoding);
// Prepend PATH
AddPrependPathToEnvironment();
// expose context to environment
foreach (var context in ExecutionContext.ExpressionValues)
{
if (context.Value is IEnvironmentContextData runtimeContext && runtimeContext != null)
{
foreach (var env in runtimeContext.GetRuntimeEnvironmentVariables())
{
Environment[env.Key] = env.Value;
}
}
}
// dump out the command
var fileName = isContainerStepHost ? shellCommand : commandPath;
#if OS_OSX
if (Environment.ContainsKey("DYLD_INSERT_LIBRARIES")) // We don't check `isContainerStepHost` because we don't support container on macOS
{
// launch `node macOSRunInvoker.js shell args` instead of `shell args` to avoid macOS SIP remove `DYLD_INSERT_LIBRARIES` when launch process
string node12 = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), "node12", "bin", $"node{IOUtil.ExeExtension}");
string macOSRunInvoker = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Bin), "macos-run-invoker.js");
arguments = $"\"{macOSRunInvoker.Replace("\"", "\\\"")}\" \"{fileName.Replace("\"", "\\\"")}\" {arguments}";
fileName = node12;
}
#endif
ExecutionContext.Debug($"{fileName} {arguments}");
using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager))
using (var stderrManager = new OutputManager(ExecutionContext, ActionCommandManager))
{
StepHost.OutputDataReceived += stdoutManager.OnDataReceived;
StepHost.ErrorDataReceived += stderrManager.OnDataReceived;
// Execute
int exitCode = await StepHost.ExecuteAsync(workingDirectory: StepHost.ResolvePathForStepHost(workingDirectory),
fileName: fileName,
arguments: arguments,
environment: Environment,
requireExitCodeZero: false,
outputEncoding: null,
killProcessOnCancel: false,
inheritConsoleHandler: !ExecutionContext.Variables.Retain_Default_Encoding,
cancellationToken: ExecutionContext.CancellationToken);
// Error
if (exitCode != 0)
{
ExecutionContext.Error($"Process completed with exit code {exitCode}.");
ExecutionContext.Result = TaskResult.Failed;
}
}
}
}
}

View File

@@ -66,6 +66,14 @@ namespace GitHub.Runner.Worker.Handlers
handler = HostContext.CreateService<IRunnerPluginHandler>();
(handler as IRunnerPluginHandler).Data = data as PluginActionExecutionData;
}
else if (data.ExecutionType == ActionExecutionType.Composite)
{
// TODO
// Runner plugin
handler = HostContext.CreateService<ICompositeActionHandler>();
// handler = CompositeHandler;
(handler as ICompositeActionHandler).Data = data as CompositeActionExecutionData;
}
else
{
// This should never happen.

View File

@@ -239,6 +239,7 @@ namespace GitHub.Runner.Worker
}
actionRunner.TryEvaluateDisplayName(contextData, context);
jobSteps.Add(actionRunner);
if (prepareResult.PreStepTracker.TryGetValue(step.Id, out var preStep))
@@ -284,6 +285,13 @@ namespace GitHub.Runner.Worker
intraActionStates.TryGetValue(actionStep.Action.Id, out var intraActionState);
actionStep.ExecutionContext = jobContext.CreateChild(actionStep.Action.Id, actionStep.DisplayName, actionStep.Action.Name, actionStep.Action.ScopeName, actionStep.Action.ContextName, intraActionState);
}
// TODO: Maybe add EvaluateStep stuff here too?
// TODO: INSERT CONVERTSTEPS FUNCTION HERE FOR EVALUATING STEPS
// Maybe we don't need to do this here do we need to initialize the job?
// context.Debug("Evaluating job evaluating steps");
// var stepsEvaluation = templateEvaluator.EvaluateSteps(contextData, context, context.ExpressionFunctions);
////////
}
List<IStep> steps = new List<IStep>();

View File

@@ -60,9 +60,11 @@ namespace GitHub.Runner.Worker
{
// Create the job execution context.
jobContext = HostContext.CreateService<IExecutionContext>();
// HERE
jobContext.InitializeJob(message, jobRequestCancellationToken);
Trace.Info("Starting the job execution context.");
jobContext.Start();
// User will see this message too
jobContext.Debug($"Starting: {message.JobDisplayName}");
runnerShutdownRegistration = HostContext.RunnerShutdownToken.Register(() =>

View File

@@ -32,7 +32,8 @@
"one-of": [
"container-runs",
"node12-runs",
"plugin-runs"
"plugin-runs",
"composite-runs"
]
},
"container-runs": {
@@ -83,6 +84,31 @@
}
}
},
"composite-runs": {
"mapping" : {
"properties": {
"using": "non-empty-string",
"steps": "steps"
}
}
},
"steps": {
"sequence": {
"item-type": "steps-item"
}
},
"steps-item": {
"one-of": [
"run-step"
]
},
"run-step": {
"mapping": {
"properties": {
"run": "non-empty-string"
}
}
},
"container-runs-context": {
"context": [
"inputs"

View File

@@ -16,6 +16,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
{
internal static class PipelineTemplateConverter
{
internal static Boolean ConvertToIfResult(
TemplateContext context,
TemplateToken ifResult)
@@ -29,7 +30,6 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
var evaluationResult = EvaluationResult.CreateIntermediateResult(null, ifResult);
return evaluationResult.IsTruthy;
}
internal static Boolean? ConvertToStepContinueOnError(
TemplateContext context,
TemplateToken token,
@@ -264,5 +264,351 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
return result;
}
//Note: originally was List<Step> but we need to change to List<ActionStep> to use the "Inputs" attribute
internal static List<ActionStep> ConvertToSteps(
TemplateContext context,
TemplateToken steps)
{
var stepsSequence = steps.AssertSequence($"job {PipelineTemplateConstants.Steps}");
var result = new List<ActionStep>();
foreach (var stepsItem in stepsSequence)
{
var step = ConvertToStep(context, stepsItem);
if (step != null) // step = null means we are hitting error during step conversion, there should be an error in context.errors
{
if (step.Enabled)
{
result.Add(step);
}
}
}
return result;
}
private static ActionStep ConvertToStep(
TemplateContext context,
TemplateToken stepsItem)
{
var step = stepsItem.AssertMapping($"{PipelineTemplateConstants.Steps} item");
var continueOnError = default(ScalarToken);
var env = default(TemplateToken);
var id = default(StringToken);
var ifCondition = default(String);
var ifToken = default(ScalarToken);
var name = default(ScalarToken);
var run = default(ScalarToken);
var scope = default(StringToken);
var timeoutMinutes = default(ScalarToken);
var uses = default(StringToken);
var with = default(TemplateToken);
var workingDir = default(ScalarToken);
var path = default(ScalarToken);
var clean = default(ScalarToken);
var fetchDepth = default(ScalarToken);
var lfs = default(ScalarToken);
var submodules = default(ScalarToken);
var shell = default(ScalarToken);
foreach (var stepProperty in step)
{
var propertyName = stepProperty.Key.AssertString($"{PipelineTemplateConstants.Steps} item key");
switch (propertyName.Value)
{
case PipelineTemplateConstants.Clean:
clean = stepProperty.Value.AssertScalar($"{PipelineTemplateConstants.Steps} item {PipelineTemplateConstants.Clean}");
break;
case PipelineTemplateConstants.ContinueOnError:
ConvertToStepContinueOnError(context, stepProperty.Value, allowExpressions: true); // Validate early if possible
continueOnError = stepProperty.Value.AssertScalar($"{PipelineTemplateConstants.Steps} {PipelineTemplateConstants.ContinueOnError}");
break;
case PipelineTemplateConstants.Env:
ConvertToStepEnvironment(context, stepProperty.Value, StringComparer.Ordinal, allowExpressions: true); // Validate early if possible
env = stepProperty.Value;
break;
case PipelineTemplateConstants.FetchDepth:
fetchDepth = stepProperty.Value.AssertScalar($"{PipelineTemplateConstants.Steps} item {PipelineTemplateConstants.FetchDepth}");
break;
case PipelineTemplateConstants.Id:
id = stepProperty.Value.AssertString($"{PipelineTemplateConstants.Steps} item {PipelineTemplateConstants.Id}");
if (!NameValidation.IsValid(id.Value, true))
{
context.Error(id, $"Step id {id.Value} is invalid. Ids must start with a letter or '_' and contain only alphanumeric characters, '-', or '_'");
}
break;
case PipelineTemplateConstants.If:
ifToken = stepProperty.Value.AssertScalar($"{PipelineTemplateConstants.Steps} item {PipelineTemplateConstants.If}");
break;
case PipelineTemplateConstants.Lfs:
lfs = stepProperty.Value.AssertScalar($"{PipelineTemplateConstants.Steps} item {PipelineTemplateConstants.Lfs}");
break;
case PipelineTemplateConstants.Name:
name = stepProperty.Value.AssertScalar($"{PipelineTemplateConstants.Steps} item {PipelineTemplateConstants.Name}");
break;
case PipelineTemplateConstants.Path:
path = stepProperty.Value.AssertScalar($"{PipelineTemplateConstants.Steps} item {PipelineTemplateConstants.Path}");
break;
case PipelineTemplateConstants.Run:
run = stepProperty.Value.AssertScalar($"{PipelineTemplateConstants.Steps} item {PipelineTemplateConstants.Run}");
break;
case PipelineTemplateConstants.Shell:
shell = stepProperty.Value.AssertScalar($"{PipelineTemplateConstants.Steps} item {PipelineTemplateConstants.Shell}");
break;
case PipelineTemplateConstants.Scope:
scope = stepProperty.Value.AssertString($"{PipelineTemplateConstants.Steps} item {PipelineTemplateConstants.Scope}");
break;
case PipelineTemplateConstants.Submodules:
submodules = stepProperty.Value.AssertScalar($"{PipelineTemplateConstants.Steps} item {PipelineTemplateConstants.Submodules}");
break;
case PipelineTemplateConstants.TimeoutMinutes:
ConvertToStepTimeout(context, stepProperty.Value, allowExpressions: true); // Validate early if possible
timeoutMinutes = stepProperty.Value.AssertScalar($"{PipelineTemplateConstants.Steps} item {PipelineTemplateConstants.TimeoutMinutes}");
break;
case PipelineTemplateConstants.Uses:
uses = stepProperty.Value.AssertString($"{PipelineTemplateConstants.Steps} item {PipelineTemplateConstants.Uses}");
break;
case PipelineTemplateConstants.With:
ConvertToStepInputs(context, stepProperty.Value, allowExpressions: true); // Validate early if possible
with = stepProperty.Value;
break;
case PipelineTemplateConstants.WorkingDirectory:
workingDir = stepProperty.Value.AssertScalar($"{PipelineTemplateConstants.Steps} item {PipelineTemplateConstants.WorkingDirectory}");
break;
default:
propertyName.AssertUnexpectedValue($"{PipelineTemplateConstants.Steps} item key"); // throws
break;
}
}
// Fixup the if-condition
var isDefaultScope = String.IsNullOrEmpty(scope?.Value);
ifCondition = ConvertToIfCondition(context, ifToken, false, isDefaultScope);
if (run != null)
{
var result = new ActionStep
{
ScopeName = scope?.Value,
ContextName = id?.Value,
ContinueOnError = continueOnError,
DisplayNameToken = name,
Condition = ifCondition,
TimeoutInMinutes = timeoutMinutes,
Environment = env,
Reference = new ScriptReference(),
};
var inputs = new MappingToken(null, null, null);
inputs.Add(new StringToken(null, null, null, PipelineConstants.ScriptStepInputs.Script), run);
if (workingDir != null)
{
inputs.Add(new StringToken(null, null, null, PipelineConstants.ScriptStepInputs.WorkingDirectory), workingDir);
}
if (shell != null)
{
inputs.Add(new StringToken(null, null, null, PipelineConstants.ScriptStepInputs.Shell), shell);
}
result.Inputs = inputs;
return result;
}
else
{
uses.AssertString($"{PipelineTemplateConstants.Steps} item {PipelineTemplateConstants.Uses}");
var result = new ActionStep
{
ScopeName = scope?.Value,
ContextName = id?.Value,
ContinueOnError = continueOnError,
DisplayNameToken = name,
Condition = ifCondition,
TimeoutInMinutes = timeoutMinutes,
Inputs = with,
Environment = env,
};
if (uses.Value.StartsWith("docker://", StringComparison.Ordinal))
{
var image = uses.Value.Substring("docker://".Length);
result.Reference = new ContainerRegistryReference { Image = image };
}
else if (uses.Value.StartsWith("./") || uses.Value.StartsWith(".\\"))
{
result.Reference = new RepositoryPathReference
{
RepositoryType = PipelineConstants.SelfAlias,
Path = uses.Value
};
}
else
{
var usesSegments = uses.Value.Split('@');
var pathSegments = usesSegments[0].Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries);
var gitRef = usesSegments.Length == 2 ? usesSegments[1] : String.Empty;
if (usesSegments.Length != 2 ||
pathSegments.Length < 2 ||
String.IsNullOrEmpty(pathSegments[0]) ||
String.IsNullOrEmpty(pathSegments[1]) ||
String.IsNullOrEmpty(gitRef))
{
// todo: loc
context.Error(uses, $"Expected format {{org}}/{{repo}}[/path]@ref. Actual '{uses.Value}'");
}
else
{
var repositoryName = $"{pathSegments[0]}/{pathSegments[1]}";
var directoryPath = pathSegments.Length > 2 ? String.Join("/", pathSegments.Skip(2)) : String.Empty;
result.Reference = new RepositoryPathReference
{
RepositoryType = RepositoryTypes.GitHub,
Name = repositoryName,
Ref = gitRef,
Path = directoryPath,
};
}
}
return result;
}
}
/// <summary>
/// When empty, default to "success()".
/// When a status function is not referenced, format as "success() &amp;&amp; &lt;CONDITION&gt;".
/// </summary>
private static String ConvertToIfCondition(
TemplateContext context,
TemplateToken token,
Boolean isJob,
Boolean isDefaultScope)
{
String condition;
if (token is null)
{
condition = null;
}
else if (token is BasicExpressionToken expressionToken)
{
condition = expressionToken.Expression;
}
else
{
var stringToken = token.AssertString($"{(isJob ? "job" : "step")} {PipelineTemplateConstants.If}");
condition = stringToken.Value;
}
if (String.IsNullOrWhiteSpace(condition))
{
return $"{PipelineTemplateConstants.Success}()";
}
var expressionParser = new ExpressionParser();
var functions = default(IFunctionInfo[]);
var namedValues = default(INamedValueInfo[]);
if (isJob)
{
namedValues = s_jobIfNamedValues;
// TODO: refactor into seperate functions
// functions = PhaseCondition.FunctionInfo;
}
else
{
namedValues = isDefaultScope ? s_stepNamedValues : s_stepInTemplateNamedValues;
functions = s_stepConditionFunctions;
}
var node = default(ExpressionNode);
try
{
node = expressionParser.CreateTree(condition, null, namedValues, functions) as ExpressionNode;
}
catch (Exception ex)
{
context.Error(token, ex);
return null;
}
if (node == null)
{
return $"{PipelineTemplateConstants.Success}()";
}
var hasStatusFunction = node.Traverse().Any(x =>
{
if (x is Function function)
{
return String.Equals(function.Name, PipelineTemplateConstants.Always, StringComparison.OrdinalIgnoreCase) ||
String.Equals(function.Name, PipelineTemplateConstants.Cancelled, StringComparison.OrdinalIgnoreCase) ||
String.Equals(function.Name, PipelineTemplateConstants.Failure, StringComparison.OrdinalIgnoreCase) ||
String.Equals(function.Name, PipelineTemplateConstants.Success, StringComparison.OrdinalIgnoreCase);
}
return false;
});
return hasStatusFunction ? condition : $"{PipelineTemplateConstants.Success}() && ({condition})";
}
private static readonly INamedValueInfo[] s_jobIfNamedValues = new INamedValueInfo[]
{
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.GitHub),
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.Needs),
};
private static readonly INamedValueInfo[] s_stepNamedValues = new INamedValueInfo[]
{
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.Strategy),
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.Matrix),
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.Steps),
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.GitHub),
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.Job),
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.Runner),
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.Env),
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.Needs),
};
private static readonly INamedValueInfo[] s_stepInTemplateNamedValues = new INamedValueInfo[]
{
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.Strategy),
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.Matrix),
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.Steps),
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.Inputs),
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.GitHub),
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.Job),
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.Runner),
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.Env),
new NamedValueInfo<NoOperationNamedValue>(PipelineTemplateConstants.Needs),
};
private static readonly IFunctionInfo[] s_stepConditionFunctions = new IFunctionInfo[]
{
new FunctionInfo<NoOperation>(PipelineTemplateConstants.Always, 0, 0),
new FunctionInfo<NoOperation>(PipelineTemplateConstants.Cancelled, 0, 0),
new FunctionInfo<NoOperation>(PipelineTemplateConstants.Failure, 0, 0),
new FunctionInfo<NoOperation>(PipelineTemplateConstants.Success, 0, 0),
new FunctionInfo<NoOperation>(PipelineTemplateConstants.HashFiles, 1, Byte.MaxValue),
};
}
}

View File

@@ -159,6 +159,44 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
return result;
}
// TODO: Add function here that says Evaluate Steps but it will return whatever ConvertToSteps returns.
// return: List<ActionStep>
// used to be evaluatesteps
public List<ActionStep> LoadSteps(
TemplateToken token,
DictionaryContextData contextData,
IList<IFunctionInfo> expressionFunctions
)
{
// Similar to EvaluateStepDisplayName() except that we pass in PipelineTemplateConstants.Steps as the type for evaluation
var result = default(List<ActionStep>);
if (token != null && token.Type != TokenType.Null)
{
var context = CreateContext(contextData, expressionFunctions, setMissingContext: false);
// This will keep it from preexpanding
// TODO: we might want to to have a bool to prevent it from filling in with missing context w/ dummy variables
// context.ExpressionFunctions.Clear();
// context.ExpressionValues.Clear();
try
{
token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.Steps, token, 0, null, omitHeader: true);
context.Errors.Check();
result = PipelineTemplateConverter.ConvertToSteps(context, token);
}
catch (Exception ex) when (!(ex is TemplateValidationException))
{
context.Errors.Add(ex);
}
context.Errors.Check();
}
return result;
}
public Dictionary<String, String> EvaluateStepEnvironment(
TemplateToken token,
DictionaryContextData contextData,
@@ -400,7 +438,8 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
private TemplateContext CreateContext(
DictionaryContextData contextData,
IList<IFunctionInfo> expressionFunctions,
IEnumerable<KeyValuePair<String, Object>> expressionState = null)
IEnumerable<KeyValuePair<String, Object>> expressionState = null,
bool setMissingContext = true)
{
var result = new TemplateContext
{
@@ -449,6 +488,8 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
// - Evaluating early when all referenced contexts are available, even though all allowed
// contexts may not yet be available. For example, evaluating step display name can often
// be performed early.
if (setMissingContext)
{
foreach (var name in s_expressionValueNames)
{
if (!result.ExpressionValues.ContainsKey(name))
@@ -463,6 +504,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating
result.ExpressionFunctions.Add(new FunctionInfo<NoOperation>(name, 0, Int32.MaxValue));
}
}
}
// Add state
if (expressionState != null)

View File

@@ -94,5 +94,12 @@ namespace GitHub.DistributedTask.Pipelines
public static readonly String Resources = "resources";
public static readonly String All = "all";
}
public static class ScriptStepInputs
{
public static readonly String Script = "script";
public static readonly String WorkingDirectory = "workingDirectory";
public static readonly String Shell = "shell";
}
}
}