From a54f380b0eac89a0dab308fec19c932634dac886 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Mon, 17 Nov 2025 19:15:46 -0600 Subject: [PATCH] Compare updated workflow parser for ActionManifestManager (#4111) --- src/Runner.Common/Constants.cs | 2 +- src/Runner.Worker/ActionManager.cs | 4 +- src/Runner.Worker/ActionManifestManager.cs | 135 ++- .../ActionManifestManagerLegacy.cs | 546 ++++++++++ .../ActionManifestManagerWrapper.cs | 701 +++++++++++++ src/Runner.Worker/ActionRunner.cs | 2 +- src/Runner.Worker/ExecutionContext.cs | 2 +- src/Runner.Worker/GlobalContext.cs | 1 + .../Handlers/CompositeActionHandler.cs | 2 +- .../Handlers/ContainerActionHandler.cs | 2 +- src/Sdk/Sdk.csproj | 4 + src/Test/L0/Worker/ActionManagerL0.cs | 12 +- src/Test/L0/Worker/ActionManifestManagerL0.cs | 63 +- .../Worker/ActionManifestManagerLegacyL0.cs | 957 ++++++++++++++++++ src/Test/L0/Worker/ActionRunnerL0.cs | 15 +- 15 files changed, 2366 insertions(+), 82 deletions(-) create mode 100644 src/Runner.Worker/ActionManifestManagerLegacy.cs create mode 100644 src/Runner.Worker/ActionManifestManagerWrapper.cs create mode 100644 src/Test/L0/Worker/ActionManifestManagerLegacyL0.cs diff --git a/src/Runner.Common/Constants.cs b/src/Runner.Common/Constants.cs index fafa773cb..d3bce0a06 100644 --- a/src/Runner.Common/Constants.cs +++ b/src/Runner.Common/Constants.cs @@ -172,7 +172,7 @@ namespace GitHub.Runner.Common public static readonly string ContainerActionRunnerTemp = "actions_container_action_runner_temp"; public static readonly string SnapshotPreflightHostedRunnerCheck = "actions_snapshot_preflight_hosted_runner_check"; public static readonly string SnapshotPreflightImageGenPoolCheck = "actions_snapshot_preflight_image_gen_pool_check"; - public static readonly string CompareTemplateEvaluator = "actions_runner_compare_template_evaluator"; + public static readonly string CompareWorkflowParser = "actions_runner_compare_workflow_parser"; } // Node version migration related constants diff --git a/src/Runner.Worker/ActionManager.cs b/src/Runner.Worker/ActionManager.cs index c2af24bbc..e38ea4d28 100644 --- a/src/Runner.Worker/ActionManager.cs +++ b/src/Runner.Worker/ActionManager.cs @@ -378,7 +378,7 @@ namespace GitHub.Runner.Worker string dockerFileLowerCase = Path.Combine(actionDirectory, "dockerfile"); if (File.Exists(manifestFile) || File.Exists(manifestFileYaml)) { - var manifestManager = HostContext.GetService(); + var manifestManager = HostContext.GetService(); if (File.Exists(manifestFile)) { definition.Data = manifestManager.Load(executionContext, manifestFile); @@ -964,7 +964,7 @@ namespace GitHub.Runner.Worker if (File.Exists(actionManifest) || File.Exists(actionManifestYaml)) { executionContext.Debug($"action.yml for action: '{actionManifest}'."); - var manifestManager = HostContext.GetService(); + var manifestManager = HostContext.GetService(); ActionDefinitionData actionDefinitionData = null; if (File.Exists(actionManifest)) { diff --git a/src/Runner.Worker/ActionManifestManager.cs b/src/Runner.Worker/ActionManifestManager.cs index c731b3d5d..014c053aa 100644 --- a/src/Runner.Worker/ActionManifestManager.cs +++ b/src/Runner.Worker/ActionManifestManager.cs @@ -2,29 +2,29 @@ using System.Collections.Generic; using System.IO; using System.Threading; +using System.Reflection; +using System.Linq; using GitHub.Runner.Common; using GitHub.Runner.Sdk; -using System.Reflection; -using GitHub.DistributedTask.Pipelines.ObjectTemplating; -using GitHub.DistributedTask.ObjectTemplating.Schema; -using GitHub.DistributedTask.ObjectTemplating; -using GitHub.DistributedTask.ObjectTemplating.Tokens; -using GitHub.DistributedTask.Pipelines.ContextData; -using System.Linq; -using Pipelines = GitHub.DistributedTask.Pipelines; +using GitHub.Actions.WorkflowParser; +using GitHub.Actions.WorkflowParser.Conversion; +using GitHub.Actions.WorkflowParser.ObjectTemplating; +using GitHub.Actions.WorkflowParser.ObjectTemplating.Schema; +using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens; +using GitHub.Actions.Expressions.Data; namespace GitHub.Runner.Worker { [ServiceLocator(Default = typeof(ActionManifestManager))] public interface IActionManifestManager : IRunnerService { - ActionDefinitionData Load(IExecutionContext executionContext, string manifestFile); + public ActionDefinitionDataNew Load(IExecutionContext executionContext, string manifestFile); - DictionaryContextData EvaluateCompositeOutputs(IExecutionContext executionContext, TemplateToken token, IDictionary extraExpressionValues); + DictionaryExpressionData EvaluateCompositeOutputs(IExecutionContext executionContext, TemplateToken token, IDictionary extraExpressionValues); - List EvaluateContainerArguments(IExecutionContext executionContext, SequenceToken token, IDictionary extraExpressionValues); + List EvaluateContainerArguments(IExecutionContext executionContext, SequenceToken token, IDictionary extraExpressionValues); - Dictionary EvaluateContainerEnvironment(IExecutionContext executionContext, MappingToken token, IDictionary extraExpressionValues); + Dictionary EvaluateContainerEnvironment(IExecutionContext executionContext, MappingToken token, IDictionary extraExpressionValues); string EvaluateDefaultInput(IExecutionContext executionContext, string inputName, TemplateToken token); } @@ -50,10 +50,10 @@ namespace GitHub.Runner.Worker Trace.Info($"Load schema file with definitions: {StringUtil.ConvertToJson(_actionManifestSchema.Definitions.Keys)}"); } - public ActionDefinitionData Load(IExecutionContext executionContext, string manifestFile) + public ActionDefinitionDataNew Load(IExecutionContext executionContext, string manifestFile) { var templateContext = CreateTemplateContext(executionContext); - ActionDefinitionData actionDefinition = new(); + ActionDefinitionDataNew actionDefinition = new(); // Clean up file name real quick // Instead of using Regex which can be computationally expensive, @@ -160,21 +160,21 @@ namespace GitHub.Runner.Worker return actionDefinition; } - public DictionaryContextData EvaluateCompositeOutputs( + public DictionaryExpressionData EvaluateCompositeOutputs( IExecutionContext executionContext, TemplateToken token, - IDictionary extraExpressionValues) + IDictionary extraExpressionValues) { - var result = default(DictionaryContextData); + DictionaryExpressionData result = null; if (token != null) { var templateContext = CreateTemplateContext(executionContext, extraExpressionValues); try { - token = TemplateEvaluator.Evaluate(templateContext, "outputs", token, 0, null, omitHeader: true); + token = TemplateEvaluator.Evaluate(templateContext, "outputs", token, 0, null); templateContext.Errors.Check(); - result = token.ToContextData().AssertDictionary("composite outputs"); + result = token.ToExpressionData().AssertDictionary("composite outputs"); } catch (Exception ex) when (!(ex is TemplateValidationException)) { @@ -184,13 +184,13 @@ namespace GitHub.Runner.Worker templateContext.Errors.Check(); } - return result ?? new DictionaryContextData(); + return result ?? new DictionaryExpressionData(); } public List EvaluateContainerArguments( IExecutionContext executionContext, SequenceToken token, - IDictionary extraExpressionValues) + IDictionary extraExpressionValues) { var result = new List(); @@ -199,7 +199,7 @@ namespace GitHub.Runner.Worker var templateContext = CreateTemplateContext(executionContext, extraExpressionValues); try { - var evaluateResult = TemplateEvaluator.Evaluate(templateContext, "container-runs-args", token, 0, null, omitHeader: true); + var evaluateResult = TemplateEvaluator.Evaluate(templateContext, "container-runs-args", token, 0, null); templateContext.Errors.Check(); Trace.Info($"Arguments evaluate result: {StringUtil.ConvertToJson(evaluateResult)}"); @@ -229,7 +229,7 @@ namespace GitHub.Runner.Worker public Dictionary EvaluateContainerEnvironment( IExecutionContext executionContext, MappingToken token, - IDictionary extraExpressionValues) + IDictionary extraExpressionValues) { var result = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -238,7 +238,7 @@ namespace GitHub.Runner.Worker var templateContext = CreateTemplateContext(executionContext, extraExpressionValues); try { - var evaluateResult = TemplateEvaluator.Evaluate(templateContext, "container-runs-env", token, 0, null, omitHeader: true); + var evaluateResult = TemplateEvaluator.Evaluate(templateContext, "container-runs-env", token, 0, null); templateContext.Errors.Check(); Trace.Info($"Environments evaluate result: {StringUtil.ConvertToJson(evaluateResult)}"); @@ -281,7 +281,7 @@ namespace GitHub.Runner.Worker var templateContext = CreateTemplateContext(executionContext); try { - var evaluateResult = TemplateEvaluator.Evaluate(templateContext, "input-default-context", token, 0, null, omitHeader: true); + var evaluateResult = TemplateEvaluator.Evaluate(templateContext, "input-default-context", token, 0, null); templateContext.Errors.Check(); Trace.Info($"Input '{inputName}': default value evaluate result: {StringUtil.ConvertToJson(evaluateResult)}"); @@ -303,7 +303,7 @@ namespace GitHub.Runner.Worker private TemplateContext CreateTemplateContext( IExecutionContext executionContext, - IDictionary extraExpressionValues = null) + IDictionary extraExpressionValues = null) { var result = new TemplateContext { @@ -314,13 +314,17 @@ namespace GitHub.Runner.Worker maxEvents: 1000000, maxBytes: 10 * 1024 * 1024), Schema = _actionManifestSchema, - TraceWriter = executionContext.ToTemplateTraceWriter(), + // TODO: Switch to real tracewriter for cutover + TraceWriter = new GitHub.Actions.WorkflowParser.ObjectTemplating.EmptyTraceWriter(), }; // Expression values from execution context foreach (var pair in executionContext.ExpressionValues) { - result.ExpressionValues[pair.Key] = pair.Value; + // Convert old PipelineContextData to new ExpressionData + var json = StringUtil.ConvertToJson(pair.Value, Newtonsoft.Json.Formatting.None); + var newValue = StringUtil.ConvertFromJson(json); + result.ExpressionValues[pair.Key] = newValue; } // Extra expression values @@ -332,10 +336,19 @@ namespace GitHub.Runner.Worker } } - // Expression functions from execution context - foreach (var item in executionContext.ExpressionFunctions) + // Expression functions + foreach (var func in executionContext.ExpressionFunctions) { - result.ExpressionFunctions.Add(item); + GitHub.Actions.Expressions.IFunctionInfo newFunc = func.Name switch + { + "always" => new GitHub.Actions.Expressions.FunctionInfo(func.Name, func.MinParameters, func.MaxParameters), + "cancelled" => new GitHub.Actions.Expressions.FunctionInfo(func.Name, func.MinParameters, func.MaxParameters), + "failure" => new GitHub.Actions.Expressions.FunctionInfo(func.Name, func.MinParameters, func.MaxParameters), + "success" => new GitHub.Actions.Expressions.FunctionInfo(func.Name, func.MinParameters, func.MaxParameters), + "hashFiles" => new GitHub.Actions.Expressions.FunctionInfo(func.Name, func.MinParameters, func.MaxParameters), + _ => throw new NotSupportedException($"Expression function '{func.Name}' is not supported in ActionManifestManager") + }; + result.ExpressionFunctions.Add(newFunc); } // Add the file table from the Execution Context @@ -368,7 +381,7 @@ namespace GitHub.Runner.Worker var postToken = default(StringToken); var postEntrypointToken = default(StringToken); var postIfToken = default(StringToken); - var steps = default(List); + var steps = default(List); foreach (var run in runsMapping) { @@ -416,7 +429,7 @@ namespace GitHub.Runner.Worker break; case "steps": var stepsToken = run.Value.AssertSequence("steps"); - steps = PipelineTemplateConverter.ConvertToSteps(templateContext, stepsToken); + steps = WorkflowTemplateConverter.ConvertToSteps(templateContext, stepsToken); templateContext.Errors.Check(); break; default: @@ -435,7 +448,7 @@ namespace GitHub.Runner.Worker } else { - return new ContainerActionExecutionData() + return new ContainerActionExecutionDataNew() { Image = imageToken.Value, Arguments = argsToken, @@ -478,11 +491,11 @@ namespace GitHub.Runner.Worker } else { - return new CompositeActionExecutionData() + return new CompositeActionExecutionDataNew() { - Steps = steps.Cast().ToList(), - PreSteps = new List(), - PostSteps = new Stack(), + Steps = steps, + PreSteps = new List(), + PostSteps = new Stack(), InitCondition = "always()", CleanupCondition = "always()", Outputs = outputs @@ -507,7 +520,7 @@ namespace GitHub.Runner.Worker private void ConvertInputs( TemplateToken inputsToken, - ActionDefinitionData actionDefinition) + ActionDefinitionDataNew actionDefinition) { actionDefinition.Inputs = new MappingToken(null, null, null); var inputsMapping = inputsToken.AssertMapping("inputs"); @@ -542,5 +555,49 @@ namespace GitHub.Runner.Worker } } } + + public sealed class ActionDefinitionDataNew + { + public string Name { get; set; } + + public string Description { get; set; } + + public MappingToken Inputs { get; set; } + + public ActionExecutionData Execution { get; set; } + + public Dictionary Deprecated { get; set; } + } + + public sealed class ContainerActionExecutionDataNew : ActionExecutionData + { + public override ActionExecutionType ExecutionType => ActionExecutionType.Container; + + public override bool HasPre => !string.IsNullOrEmpty(Pre); + public override bool HasPost => !string.IsNullOrEmpty(Post); + + public string Image { get; set; } + + public string EntryPoint { get; set; } + + public SequenceToken Arguments { get; set; } + + public MappingToken Environment { get; set; } + + public string Pre { get; set; } + + public string Post { get; set; } + } + + public sealed class CompositeActionExecutionDataNew : ActionExecutionData + { + public override ActionExecutionType ExecutionType => ActionExecutionType.Composite; + public override bool HasPre => PreSteps.Count > 0; + public override bool HasPost => PostSteps.Count > 0; + public List PreSteps { get; set; } + public List Steps { get; set; } + public Stack PostSteps { get; set; } + public MappingToken Outputs { get; set; } + } } diff --git a/src/Runner.Worker/ActionManifestManagerLegacy.cs b/src/Runner.Worker/ActionManifestManagerLegacy.cs new file mode 100644 index 000000000..89d9ae8b5 --- /dev/null +++ b/src/Runner.Worker/ActionManifestManagerLegacy.cs @@ -0,0 +1,546 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using GitHub.Runner.Common; +using GitHub.Runner.Sdk; +using System.Reflection; +using GitHub.DistributedTask.Pipelines.ObjectTemplating; +using GitHub.DistributedTask.ObjectTemplating.Schema; +using GitHub.DistributedTask.ObjectTemplating; +using GitHub.DistributedTask.ObjectTemplating.Tokens; +using GitHub.DistributedTask.Pipelines.ContextData; +using System.Linq; +using Pipelines = GitHub.DistributedTask.Pipelines; + +namespace GitHub.Runner.Worker +{ + [ServiceLocator(Default = typeof(ActionManifestManagerLegacy))] + public interface IActionManifestManagerLegacy : IRunnerService + { + ActionDefinitionData Load(IExecutionContext executionContext, string manifestFile); + + DictionaryContextData EvaluateCompositeOutputs(IExecutionContext executionContext, TemplateToken token, IDictionary extraExpressionValues); + + List EvaluateContainerArguments(IExecutionContext executionContext, SequenceToken token, IDictionary extraExpressionValues); + + Dictionary EvaluateContainerEnvironment(IExecutionContext executionContext, MappingToken token, IDictionary extraExpressionValues); + + string EvaluateDefaultInput(IExecutionContext executionContext, string inputName, TemplateToken token); + } + + public sealed class ActionManifestManagerLegacy : RunnerService, IActionManifestManagerLegacy + { + private TemplateSchema _actionManifestSchema; + public override void Initialize(IHostContext hostContext) + { + base.Initialize(hostContext); + + var assembly = Assembly.GetExecutingAssembly(); + var json = default(string); + using (var stream = assembly.GetManifestResourceStream("GitHub.Runner.Worker.action_yaml.json")) + using (var streamReader = new StreamReader(stream)) + { + json = streamReader.ReadToEnd(); + } + + var objectReader = new JsonObjectReader(null, json); + _actionManifestSchema = TemplateSchema.Load(objectReader); + ArgUtil.NotNull(_actionManifestSchema, nameof(_actionManifestSchema)); + Trace.Info($"Load schema file with definitions: {StringUtil.ConvertToJson(_actionManifestSchema.Definitions.Keys)}"); + } + + public ActionDefinitionData Load(IExecutionContext executionContext, string manifestFile) + { + var templateContext = CreateTemplateContext(executionContext); + ActionDefinitionData actionDefinition = new(); + + // Clean up file name real quick + // Instead of using Regex which can be computationally expensive, + // we can just remove the # of characters from the fileName according to the length of the basePath + string basePath = HostContext.GetDirectory(WellKnownDirectory.Actions); + string fileRelativePath = manifestFile; + if (manifestFile.Contains(basePath)) + { + fileRelativePath = manifestFile.Remove(0, basePath.Length + 1); + } + + try + { + var token = default(TemplateToken); + + // Get the file ID + var fileId = templateContext.GetFileId(fileRelativePath); + + // Add this file to the FileTable in executionContext if it hasn't been added already + // we use > since fileID is 1 indexed + if (fileId > executionContext.Global.FileTable.Count) + { + executionContext.Global.FileTable.Add(fileRelativePath); + } + + // Read the file + var fileContent = File.ReadAllText(manifestFile); + using (var stringReader = new StringReader(fileContent)) + { + var yamlObjectReader = new YamlObjectReader(fileId, stringReader); + token = TemplateReader.Read(templateContext, "action-root", yamlObjectReader, fileId, out _); + } + + var actionMapping = token.AssertMapping("action manifest root"); + var actionOutputs = default(MappingToken); + var actionRunValueToken = default(TemplateToken); + + foreach (var actionPair in actionMapping) + { + var propertyName = actionPair.Key.AssertString($"action.yml property key"); + + switch (propertyName.Value) + { + case "name": + actionDefinition.Name = actionPair.Value.AssertString("name").Value; + break; + + case "outputs": + actionOutputs = actionPair.Value.AssertMapping("outputs"); + break; + + case "description": + actionDefinition.Description = actionPair.Value.AssertString("description").Value; + break; + + case "inputs": + ConvertInputs(actionPair.Value, actionDefinition); + break; + + case "runs": + // Defer runs token evaluation to after for loop to ensure that order of outputs doesn't matter. + actionRunValueToken = actionPair.Value; + break; + + default: + Trace.Info($"Ignore action property {propertyName}."); + break; + } + } + + // Evaluate Runs Last + if (actionRunValueToken != null) + { + actionDefinition.Execution = ConvertRuns(executionContext, templateContext, actionRunValueToken, fileRelativePath, actionOutputs); + } + } + catch (Exception ex) + { + Trace.Error(ex); + templateContext.Errors.Add(ex); + } + + if (templateContext.Errors.Count > 0) + { + foreach (var error in templateContext.Errors) + { + Trace.Error($"Action.yml load error: {error.Message}"); + executionContext.Error(error.Message); + } + + throw new ArgumentException($"Failed to load {fileRelativePath}"); + } + + if (actionDefinition.Execution == null) + { + executionContext.Debug($"Loaded action.yml file: {StringUtil.ConvertToJson(actionDefinition)}"); + throw new ArgumentException($"Top level 'runs:' section is required for {fileRelativePath}"); + } + else + { + Trace.Info($"Loaded action.yml file: {StringUtil.ConvertToJson(actionDefinition)}"); + } + + return actionDefinition; + } + + public DictionaryContextData EvaluateCompositeOutputs( + IExecutionContext executionContext, + TemplateToken token, + IDictionary extraExpressionValues) + { + var result = default(DictionaryContextData); + + if (token != null) + { + var templateContext = CreateTemplateContext(executionContext, extraExpressionValues); + try + { + token = TemplateEvaluator.Evaluate(templateContext, "outputs", token, 0, null, omitHeader: true); + templateContext.Errors.Check(); + result = token.ToContextData().AssertDictionary("composite outputs"); + } + catch (Exception ex) when (!(ex is TemplateValidationException)) + { + templateContext.Errors.Add(ex); + } + + templateContext.Errors.Check(); + } + + return result ?? new DictionaryContextData(); + } + + public List EvaluateContainerArguments( + IExecutionContext executionContext, + SequenceToken token, + IDictionary extraExpressionValues) + { + var result = new List(); + + if (token != null) + { + var templateContext = CreateTemplateContext(executionContext, extraExpressionValues); + try + { + var evaluateResult = TemplateEvaluator.Evaluate(templateContext, "container-runs-args", token, 0, null, omitHeader: true); + templateContext.Errors.Check(); + + Trace.Info($"Arguments evaluate result: {StringUtil.ConvertToJson(evaluateResult)}"); + + // Sequence + var args = evaluateResult.AssertSequence("container args"); + + foreach (var arg in args) + { + var str = arg.AssertString("container arg").Value; + result.Add(str); + Trace.Info($"Add argument {str}"); + } + } + catch (Exception ex) when (!(ex is TemplateValidationException)) + { + Trace.Error(ex); + templateContext.Errors.Add(ex); + } + + templateContext.Errors.Check(); + } + + return result; + } + + public Dictionary EvaluateContainerEnvironment( + IExecutionContext executionContext, + MappingToken token, + IDictionary extraExpressionValues) + { + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (token != null) + { + var templateContext = CreateTemplateContext(executionContext, extraExpressionValues); + try + { + var evaluateResult = TemplateEvaluator.Evaluate(templateContext, "container-runs-env", token, 0, null, omitHeader: true); + templateContext.Errors.Check(); + + Trace.Info($"Environments evaluate result: {StringUtil.ConvertToJson(evaluateResult)}"); + + // Mapping + var mapping = evaluateResult.AssertMapping("container env"); + + foreach (var pair in mapping) + { + // Literal key + var key = pair.Key.AssertString("container env key"); + + // Literal value + var value = pair.Value.AssertString("container env value"); + result[key.Value] = value.Value; + + Trace.Info($"Add env {key} = {value}"); + } + } + catch (Exception ex) when (!(ex is TemplateValidationException)) + { + Trace.Error(ex); + templateContext.Errors.Add(ex); + } + + templateContext.Errors.Check(); + } + + return result; + } + + public string EvaluateDefaultInput( + IExecutionContext executionContext, + string inputName, + TemplateToken token) + { + string result = ""; + if (token != null) + { + var templateContext = CreateTemplateContext(executionContext); + try + { + var evaluateResult = TemplateEvaluator.Evaluate(templateContext, "input-default-context", token, 0, null, omitHeader: true); + templateContext.Errors.Check(); + + Trace.Info($"Input '{inputName}': default value evaluate result: {StringUtil.ConvertToJson(evaluateResult)}"); + + // String + result = evaluateResult.AssertString($"default value for input '{inputName}'").Value; + } + catch (Exception ex) when (!(ex is TemplateValidationException)) + { + Trace.Error(ex); + templateContext.Errors.Add(ex); + } + + templateContext.Errors.Check(); + } + + return result; + } + + private TemplateContext CreateTemplateContext( + IExecutionContext executionContext, + IDictionary extraExpressionValues = null) + { + var result = new TemplateContext + { + CancellationToken = CancellationToken.None, + Errors = new TemplateValidationErrors(10, int.MaxValue), // Don't truncate error messages otherwise we might not scrub secrets correctly + Memory = new TemplateMemory( + maxDepth: 100, + maxEvents: 1000000, + maxBytes: 10 * 1024 * 1024), + Schema = _actionManifestSchema, + TraceWriter = executionContext.ToTemplateTraceWriter(), + }; + + // Expression values from execution context + foreach (var pair in executionContext.ExpressionValues) + { + result.ExpressionValues[pair.Key] = pair.Value; + } + + // Extra expression values + if (extraExpressionValues?.Count > 0) + { + foreach (var pair in extraExpressionValues) + { + result.ExpressionValues[pair.Key] = pair.Value; + } + } + + // Expression functions from execution context + foreach (var item in executionContext.ExpressionFunctions) + { + result.ExpressionFunctions.Add(item); + } + + // Add the file table from the Execution Context + for (var i = 0; i < executionContext.Global.FileTable.Count; i++) + { + result.GetFileId(executionContext.Global.FileTable[i]); + } + + return result; + } + + private ActionExecutionData ConvertRuns( + IExecutionContext executionContext, + TemplateContext templateContext, + TemplateToken inputsToken, + String fileRelativePath, + MappingToken outputs = null) + { + var runsMapping = inputsToken.AssertMapping("runs"); + var usingToken = default(StringToken); + var imageToken = default(StringToken); + var argsToken = default(SequenceToken); + var entrypointToken = default(StringToken); + var envToken = default(MappingToken); + var mainToken = default(StringToken); + var pluginToken = default(StringToken); + var preToken = default(StringToken); + var preEntrypointToken = default(StringToken); + var preIfToken = default(StringToken); + var postToken = default(StringToken); + var postEntrypointToken = default(StringToken); + var postIfToken = default(StringToken); + var steps = default(List); + + foreach (var run in runsMapping) + { + var runsKey = run.Key.AssertString("runs key").Value; + switch (runsKey) + { + case "using": + usingToken = run.Value.AssertString("using"); + break; + case "image": + imageToken = run.Value.AssertString("image"); + break; + case "args": + argsToken = run.Value.AssertSequence("args"); + break; + case "entrypoint": + entrypointToken = run.Value.AssertString("entrypoint"); + break; + case "env": + envToken = run.Value.AssertMapping("env"); + break; + case "main": + mainToken = run.Value.AssertString("main"); + break; + case "plugin": + pluginToken = run.Value.AssertString("plugin"); + break; + case "post": + postToken = run.Value.AssertString("post"); + break; + case "post-entrypoint": + postEntrypointToken = run.Value.AssertString("post-entrypoint"); + break; + case "post-if": + postIfToken = run.Value.AssertString("post-if"); + break; + case "pre": + preToken = run.Value.AssertString("pre"); + break; + case "pre-entrypoint": + preEntrypointToken = run.Value.AssertString("pre-entrypoint"); + break; + case "pre-if": + preIfToken = run.Value.AssertString("pre-if"); + break; + case "steps": + var stepsToken = run.Value.AssertSequence("steps"); + steps = PipelineTemplateConverter.ConvertToSteps(templateContext, stepsToken); + templateContext.Errors.Check(); + break; + default: + Trace.Info($"Ignore run property {runsKey}."); + break; + } + } + + if (usingToken != null) + { + if (string.Equals(usingToken.Value, "docker", StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrEmpty(imageToken?.Value)) + { + throw new ArgumentNullException($"You are using a Container Action but an image is not provided in {fileRelativePath}."); + } + else + { + return new ContainerActionExecutionData() + { + Image = imageToken.Value, + Arguments = argsToken, + EntryPoint = entrypointToken?.Value, + Environment = envToken, + Pre = preEntrypointToken?.Value, + InitCondition = preIfToken?.Value ?? "always()", + Post = postEntrypointToken?.Value, + CleanupCondition = postIfToken?.Value ?? "always()" + }; + } + } + else if (string.Equals(usingToken.Value, "node12", StringComparison.OrdinalIgnoreCase) || + string.Equals(usingToken.Value, "node16", StringComparison.OrdinalIgnoreCase) || + string.Equals(usingToken.Value, "node20", StringComparison.OrdinalIgnoreCase) || + string.Equals(usingToken.Value, "node24", StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrEmpty(mainToken?.Value)) + { + throw new ArgumentNullException($"You are using a JavaScript Action but there is not an entry JavaScript file provided in {fileRelativePath}."); + } + else + { + return new NodeJSActionExecutionData() + { + NodeVersion = usingToken.Value, + Script = mainToken.Value, + Pre = preToken?.Value, + InitCondition = preIfToken?.Value ?? "always()", + Post = postToken?.Value, + CleanupCondition = postIfToken?.Value ?? "always()" + }; + } + } + else if (string.Equals(usingToken.Value, "composite", StringComparison.OrdinalIgnoreCase)) + { + if (steps == null) + { + throw new ArgumentNullException($"You are using a composite action but there are no steps provided in {fileRelativePath}."); + } + else + { + return new CompositeActionExecutionData() + { + Steps = steps.Cast().ToList(), + PreSteps = new List(), + PostSteps = new Stack(), + InitCondition = "always()", + CleanupCondition = "always()", + Outputs = outputs + }; + } + } + else + { + throw new ArgumentOutOfRangeException($"'using: {usingToken.Value}' is not supported, use 'docker', 'node12', 'node16', 'node20' or 'node24' instead."); + } + } + else if (pluginToken != null) + { + return new PluginActionExecutionData() + { + Plugin = pluginToken.Value + }; + } + + throw new NotSupportedException("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20' or 'node24'."); + } + + private void ConvertInputs( + TemplateToken inputsToken, + ActionDefinitionData actionDefinition) + { + actionDefinition.Inputs = new MappingToken(null, null, null); + var inputsMapping = inputsToken.AssertMapping("inputs"); + foreach (var input in inputsMapping) + { + bool hasDefault = false; + var inputName = input.Key.AssertString("input name"); + var inputMetadata = input.Value.AssertMapping("input metadata"); + foreach (var metadata in inputMetadata) + { + var metadataName = metadata.Key.AssertString("input metadata").Value; + if (string.Equals(metadataName, "default", StringComparison.OrdinalIgnoreCase)) + { + hasDefault = true; + actionDefinition.Inputs.Add(inputName, metadata.Value); + } + else if (string.Equals(metadataName, "deprecationMessage", StringComparison.OrdinalIgnoreCase)) + { + if (actionDefinition.Deprecated == null) + { + actionDefinition.Deprecated = new Dictionary(); + } + var message = metadata.Value.AssertString("input deprecationMessage"); + actionDefinition.Deprecated.Add(inputName.Value, message.Value); + } + } + + if (!hasDefault) + { + actionDefinition.Inputs.Add(inputName, new StringToken(null, null, null, string.Empty)); + } + } + } + } +} + diff --git a/src/Runner.Worker/ActionManifestManagerWrapper.cs b/src/Runner.Worker/ActionManifestManagerWrapper.cs new file mode 100644 index 000000000..aa265dbf4 --- /dev/null +++ b/src/Runner.Worker/ActionManifestManagerWrapper.cs @@ -0,0 +1,701 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using GitHub.Actions.WorkflowParser; +using GitHub.DistributedTask.Pipelines; +using GitHub.DistributedTask.Pipelines.ContextData; +using GitHub.DistributedTask.ObjectTemplating.Tokens; +using GitHub.DistributedTask.WebApi; +using GitHub.Runner.Common; +using GitHub.Runner.Sdk; +using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating; + +namespace GitHub.Runner.Worker +{ + [ServiceLocator(Default = typeof(ActionManifestManagerWrapper))] + public interface IActionManifestManagerWrapper : IRunnerService + { + ActionDefinitionData Load(IExecutionContext executionContext, string manifestFile); + + DictionaryContextData EvaluateCompositeOutputs(IExecutionContext executionContext, TemplateToken token, IDictionary extraExpressionValues); + + List EvaluateContainerArguments(IExecutionContext executionContext, SequenceToken token, IDictionary extraExpressionValues); + + Dictionary EvaluateContainerEnvironment(IExecutionContext executionContext, MappingToken token, IDictionary extraExpressionValues); + + string EvaluateDefaultInput(IExecutionContext executionContext, string inputName, TemplateToken token); + } + + public sealed class ActionManifestManagerWrapper : RunnerService, IActionManifestManagerWrapper + { + private IActionManifestManagerLegacy _legacyManager; + private IActionManifestManager _newManager; + + public override void Initialize(IHostContext hostContext) + { + base.Initialize(hostContext); + _legacyManager = hostContext.GetService(); + _newManager = hostContext.GetService(); + } + + public ActionDefinitionData Load(IExecutionContext executionContext, string manifestFile) + { + return EvaluateAndCompare( + executionContext, + "Load", + () => _legacyManager.Load(executionContext, manifestFile), + () => ConvertToLegacyActionDefinitionData(_newManager.Load(executionContext, manifestFile)), + (legacyResult, newResult) => CompareActionDefinition(legacyResult, newResult)); + } + + public DictionaryContextData EvaluateCompositeOutputs( + IExecutionContext executionContext, + TemplateToken token, + IDictionary extraExpressionValues) + { + return EvaluateAndCompare( + executionContext, + "EvaluateCompositeOutputs", + () => _legacyManager.EvaluateCompositeOutputs(executionContext, token, extraExpressionValues), + () => ConvertToLegacyContextData(_newManager.EvaluateCompositeOutputs(executionContext, ConvertToNewToken(token), ConvertToNewExpressionValues(extraExpressionValues))), + (legacyResult, newResult) => CompareDictionaryContextData(legacyResult, newResult)); + } + + public List EvaluateContainerArguments( + IExecutionContext executionContext, + SequenceToken token, + IDictionary extraExpressionValues) + { + return EvaluateAndCompare( + executionContext, + "EvaluateContainerArguments", + () => _legacyManager.EvaluateContainerArguments(executionContext, token, extraExpressionValues), + () => _newManager.EvaluateContainerArguments(executionContext, ConvertToNewToken(token) as GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.SequenceToken, ConvertToNewExpressionValues(extraExpressionValues)), + (legacyResult, newResult) => CompareLists(legacyResult, newResult, "ContainerArguments")); + } + + public Dictionary EvaluateContainerEnvironment( + IExecutionContext executionContext, + MappingToken token, + IDictionary extraExpressionValues) + { + return EvaluateAndCompare( + executionContext, + "EvaluateContainerEnvironment", + () => _legacyManager.EvaluateContainerEnvironment(executionContext, token, extraExpressionValues), + () => _newManager.EvaluateContainerEnvironment(executionContext, ConvertToNewToken(token) as GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.MappingToken, ConvertToNewExpressionValues(extraExpressionValues)), + (legacyResult, newResult) => { + var trace = HostContext.GetTrace(nameof(ActionManifestManagerWrapper)); + return CompareDictionaries(trace, legacyResult, newResult, "ContainerEnvironment"); + }); + } + + public string EvaluateDefaultInput( + IExecutionContext executionContext, + string inputName, + TemplateToken token) + { + return EvaluateAndCompare( + executionContext, + "EvaluateDefaultInput", + () => _legacyManager.EvaluateDefaultInput(executionContext, inputName, token), + () => _newManager.EvaluateDefaultInput(executionContext, inputName, ConvertToNewToken(token)), + (legacyResult, newResult) => string.Equals(legacyResult, newResult, StringComparison.Ordinal)); + } + + // Conversion helper methods + private ActionDefinitionData ConvertToLegacyActionDefinitionData(ActionDefinitionDataNew newData) + { + if (newData == null) + { + return null; + } + + return new ActionDefinitionData + { + Name = newData.Name, + Description = newData.Description, + Inputs = ConvertToLegacyToken(newData.Inputs), + Deprecated = newData.Deprecated, + Execution = ConvertToLegacyExecution(newData.Execution) + }; + } + + private ActionExecutionData ConvertToLegacyExecution(ActionExecutionData execution) + { + if (execution == null) + { + return null; + } + + // Handle different execution types + if (execution is ContainerActionExecutionDataNew containerNew) + { + return new ContainerActionExecutionData + { + Image = containerNew.Image, + EntryPoint = containerNew.EntryPoint, + Arguments = ConvertToLegacyToken(containerNew.Arguments), + Environment = ConvertToLegacyToken(containerNew.Environment), + Pre = containerNew.Pre, + Post = containerNew.Post, + InitCondition = containerNew.InitCondition, + CleanupCondition = containerNew.CleanupCondition + }; + } + else if (execution is CompositeActionExecutionDataNew compositeNew) + { + return new CompositeActionExecutionData + { + Steps = ConvertToLegacySteps(compositeNew.Steps), + Outputs = ConvertToLegacyToken(compositeNew.Outputs) + }; + } + else + { + // For NodeJS and Plugin execution, they don't use new token types, so just return as-is + return execution; + } + } + + private List ConvertToLegacySteps(List newSteps) + { + if (newSteps == null) + { + return null; + } + + // Serialize new steps and deserialize to old steps + var json = StringUtil.ConvertToJson(newSteps, Newtonsoft.Json.Formatting.None); + return StringUtil.ConvertFromJson>(json); + } + + private T ConvertToLegacyToken(GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.TemplateToken newToken) where T : TemplateToken + { + if (newToken == null) + { + return null; + } + + // Serialize and deserialize to convert between token types + var json = StringUtil.ConvertToJson(newToken, Newtonsoft.Json.Formatting.None); + return StringUtil.ConvertFromJson(json); + } + + private GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens.TemplateToken ConvertToNewToken(TemplateToken legacyToken) + { + if (legacyToken == null) + { + return null; + } + + var json = StringUtil.ConvertToJson(legacyToken, Newtonsoft.Json.Formatting.None); + return StringUtil.ConvertFromJson(json); + } + + private IDictionary ConvertToNewExpressionValues(IDictionary legacyValues) + { + if (legacyValues == null) + { + return null; + } + + var json = StringUtil.ConvertToJson(legacyValues, Newtonsoft.Json.Formatting.None); + return StringUtil.ConvertFromJson>(json); + } + + private T ConvertToLegacyContextData(GitHub.Actions.Expressions.Data.ExpressionData newData) where T : PipelineContextData + { + if (newData == null) + { + return null; + } + + var json = StringUtil.ConvertToJson(newData, Newtonsoft.Json.Formatting.None); + return StringUtil.ConvertFromJson(json); + } + + // Comparison helper methods + private TLegacy EvaluateAndCompare( + IExecutionContext context, + string methodName, + Func legacyEvaluator, + Func newEvaluator, + Func resultComparer) + { + // Legacy only? + if (!((context.Global.Variables.GetBoolean(Constants.Runner.Features.CompareWorkflowParser) ?? false) + || StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_COMPARE_WORKFLOW_PARSER")))) + { + return legacyEvaluator(); + } + + var trace = HostContext.GetTrace(nameof(ActionManifestManagerWrapper)); + + // Legacy evaluator + var legacyException = default(Exception); + var legacyResult = default(TLegacy); + try + { + legacyResult = legacyEvaluator(); + } + catch (Exception ex) + { + legacyException = ex; + } + + // Compare with new evaluator + try + { + ArgUtil.NotNull(context, nameof(context)); + trace.Info(methodName); + + // New evaluator + var newException = default(Exception); + var newResult = default(TNew); + try + { + newResult = newEvaluator(); + } + catch (Exception ex) + { + newException = ex; + } + + // Compare results or exceptions + if (legacyException != null || newException != null) + { + // Either one or both threw exceptions - compare them + if (!CompareExceptions(trace, legacyException, newException)) + { + trace.Info($"{methodName} exception mismatch"); + RecordMismatch(context, $"{methodName}"); + } + } + else + { + // Both succeeded - compare results + // Skip comparison if new implementation returns null (not yet implemented) + if (newResult != null && !resultComparer(legacyResult, newResult)) + { + trace.Info($"{methodName} mismatch"); + RecordMismatch(context, $"{methodName}"); + } + } + } + catch (Exception ex) + { + trace.Info($"Comparison failed: {ex.Message}"); + RecordComparisonError(context, $"{methodName}: {ex.Message}"); + } + + // Re-throw legacy exception if any + if (legacyException != null) + { + throw legacyException; + } + + return legacyResult; + } + + private void RecordMismatch(IExecutionContext context, string methodName) + { + if (!context.Global.HasActionManifestMismatch) + { + context.Global.HasActionManifestMismatch = true; + var telemetry = new JobTelemetry { Type = JobTelemetryType.General, Message = $"ActionManifestMismatch: {methodName}" }; + context.Global.JobTelemetry.Add(telemetry); + } + } + + private void RecordComparisonError(IExecutionContext context, string errorDetails) + { + if (!context.Global.HasActionManifestMismatch) + { + context.Global.HasActionManifestMismatch = true; + var telemetry = new JobTelemetry { Type = JobTelemetryType.General, Message = $"ActionManifestComparisonError: {errorDetails}" }; + context.Global.JobTelemetry.Add(telemetry); + } + } + + private bool CompareActionDefinition(ActionDefinitionData legacyResult, ActionDefinitionData newResult) + { + var trace = HostContext.GetTrace(nameof(ActionManifestManagerWrapper)); + if (legacyResult == null && newResult == null) + { + return true; + } + + if (legacyResult == null || newResult == null) + { + trace.Info($"CompareActionDefinition mismatch - one result is null (legacy={legacyResult == null}, new={newResult == null})"); + return false; + } + + if (!string.Equals(legacyResult.Name, newResult.Name, StringComparison.Ordinal)) + { + trace.Info($"CompareActionDefinition mismatch - Name differs (legacy='{legacyResult.Name}', new='{newResult.Name}')"); + return false; + } + + if (!string.Equals(legacyResult.Description, newResult.Description, StringComparison.Ordinal)) + { + trace.Info($"CompareActionDefinition mismatch - Description differs (legacy='{legacyResult.Description}', new='{newResult.Description}')"); + return false; + } + + // Compare Inputs token + var legacyInputsJson = legacyResult.Inputs != null ? StringUtil.ConvertToJson(legacyResult.Inputs) : null; + var newInputsJson = newResult.Inputs != null ? StringUtil.ConvertToJson(newResult.Inputs) : null; + if (!string.Equals(legacyInputsJson, newInputsJson, StringComparison.Ordinal)) + { + trace.Info($"CompareActionDefinition mismatch - Inputs differ"); + return false; + } + + // Compare Deprecated + if (!CompareDictionaries(trace, legacyResult.Deprecated, newResult.Deprecated, "Deprecated")) + { + return false; + } + + // Compare Execution + if (!CompareExecution(trace, legacyResult.Execution, newResult.Execution)) + { + return false; + } + + return true; + } + + private bool CompareExecution(Tracing trace, ActionExecutionData legacy, ActionExecutionData newExecution) + { + if (legacy == null && newExecution == null) + { + return true; + } + + if (legacy == null || newExecution == null) + { + trace.Info($"CompareExecution mismatch - one is null (legacy={legacy == null}, new={newExecution == null})"); + return false; + } + + if (legacy.GetType() != newExecution.GetType()) + { + trace.Info($"CompareExecution mismatch - different types (legacy={legacy.GetType().Name}, new={newExecution.GetType().Name})"); + return false; + } + + // Compare based on type + if (legacy is NodeJSActionExecutionData legacyNode && newExecution is NodeJSActionExecutionData newNode) + { + return CompareNodeJSExecution(trace, legacyNode, newNode); + } + else if (legacy is ContainerActionExecutionData legacyContainer && newExecution is ContainerActionExecutionData newContainer) + { + return CompareContainerExecution(trace, legacyContainer, newContainer); + } + else if (legacy is CompositeActionExecutionData legacyComposite && newExecution is CompositeActionExecutionData newComposite) + { + return CompareCompositeExecution(trace, legacyComposite, newComposite); + } + else if (legacy is PluginActionExecutionData legacyPlugin && newExecution is PluginActionExecutionData newPlugin) + { + return ComparePluginExecution(trace, legacyPlugin, newPlugin); + } + + return true; + } + + private bool CompareNodeJSExecution(Tracing trace, NodeJSActionExecutionData legacy, NodeJSActionExecutionData newExecution) + { + if (!string.Equals(legacy.NodeVersion, newExecution.NodeVersion, StringComparison.Ordinal)) + { + trace.Info($"CompareNodeJSExecution mismatch - NodeVersion differs (legacy='{legacy.NodeVersion}', new='{newExecution.NodeVersion}')"); + return false; + } + + if (!string.Equals(legacy.Script, newExecution.Script, StringComparison.Ordinal)) + { + trace.Info($"CompareNodeJSExecution mismatch - Script differs (legacy='{legacy.Script}', new='{newExecution.Script}')"); + return false; + } + + if (!string.Equals(legacy.Pre, newExecution.Pre, StringComparison.Ordinal)) + { + trace.Info($"CompareNodeJSExecution mismatch - Pre differs"); + return false; + } + + if (!string.Equals(legacy.Post, newExecution.Post, StringComparison.Ordinal)) + { + trace.Info($"CompareNodeJSExecution mismatch - Post differs"); + return false; + } + + if (!string.Equals(legacy.InitCondition, newExecution.InitCondition, StringComparison.Ordinal)) + { + trace.Info($"CompareNodeJSExecution mismatch - InitCondition differs"); + return false; + } + + if (!string.Equals(legacy.CleanupCondition, newExecution.CleanupCondition, StringComparison.Ordinal)) + { + trace.Info($"CompareNodeJSExecution mismatch - CleanupCondition differs"); + return false; + } + + return true; + } + + private bool CompareContainerExecution(Tracing trace, ContainerActionExecutionData legacy, ContainerActionExecutionData newExecution) + { + if (!string.Equals(legacy.Image, newExecution.Image, StringComparison.Ordinal)) + { + trace.Info($"CompareContainerExecution mismatch - Image differs"); + return false; + } + + if (!string.Equals(legacy.EntryPoint, newExecution.EntryPoint, StringComparison.Ordinal)) + { + trace.Info($"CompareContainerExecution mismatch - EntryPoint differs"); + return false; + } + + // Compare Arguments token + var legacyArgsJson = legacy.Arguments != null ? StringUtil.ConvertToJson(legacy.Arguments) : null; + var newArgsJson = newExecution.Arguments != null ? StringUtil.ConvertToJson(newExecution.Arguments) : null; + if (!string.Equals(legacyArgsJson, newArgsJson, StringComparison.Ordinal)) + { + trace.Info($"CompareContainerExecution mismatch - Arguments differ"); + return false; + } + + // Compare Environment token + var legacyEnvJson = legacy.Environment != null ? StringUtil.ConvertToJson(legacy.Environment) : null; + var newEnvJson = newExecution.Environment != null ? StringUtil.ConvertToJson(newExecution.Environment) : null; + if (!string.Equals(legacyEnvJson, newEnvJson, StringComparison.Ordinal)) + { + trace.Info($"CompareContainerExecution mismatch - Environment differs"); + return false; + } + + return true; + } + + private bool CompareCompositeExecution(Tracing trace, CompositeActionExecutionData legacy, CompositeActionExecutionData newExecution) + { + // Compare Steps + if (legacy.Steps?.Count != newExecution.Steps?.Count) + { + trace.Info($"CompareCompositeExecution mismatch - Steps.Count differs (legacy={legacy.Steps?.Count}, new={newExecution.Steps?.Count})"); + return false; + } + + // Compare Outputs token + var legacyOutputsJson = legacy.Outputs != null ? StringUtil.ConvertToJson(legacy.Outputs) : null; + var newOutputsJson = newExecution.Outputs != null ? StringUtil.ConvertToJson(newExecution.Outputs) : null; + if (!string.Equals(legacyOutputsJson, newOutputsJson, StringComparison.Ordinal)) + { + trace.Info($"CompareCompositeExecution mismatch - Outputs differ"); + return false; + } + + return true; + } + + private bool ComparePluginExecution(Tracing trace, PluginActionExecutionData legacy, PluginActionExecutionData newExecution) + { + if (!string.Equals(legacy.Plugin, newExecution.Plugin, StringComparison.Ordinal)) + { + trace.Info($"ComparePluginExecution mismatch - Plugin differs"); + return false; + } + + return true; + } + + private bool CompareDictionaryContextData(DictionaryContextData legacy, DictionaryContextData newData) + { + var trace = HostContext.GetTrace(nameof(ActionManifestManagerWrapper)); + if (legacy == null && newData == null) + { + return true; + } + + if (legacy == null || newData == null) + { + trace.Info($"CompareDictionaryContextData mismatch - one is null (legacy={legacy == null}, new={newData == null})"); + return false; + } + + var legacyJson = StringUtil.ConvertToJson(legacy); + var newJson = StringUtil.ConvertToJson(newData); + + if (!string.Equals(legacyJson, newJson, StringComparison.Ordinal)) + { + trace.Info($"CompareDictionaryContextData mismatch"); + return false; + } + + return true; + } + + private bool CompareLists(IList legacyList, IList newList, string fieldName) + { + var trace = HostContext.GetTrace(nameof(ActionManifestManagerWrapper)); + if (legacyList == null && newList == null) + { + return true; + } + + if (legacyList == null || newList == null) + { + trace.Info($"CompareLists mismatch - {fieldName} - one is null (legacy={legacyList == null}, new={newList == null})"); + return false; + } + + if (legacyList.Count != newList.Count) + { + trace.Info($"CompareLists mismatch - {fieldName}.Count differs (legacy={legacyList.Count}, new={newList.Count})"); + return false; + } + + for (int i = 0; i < legacyList.Count; i++) + { + if (!string.Equals(legacyList[i], newList[i], StringComparison.Ordinal)) + { + trace.Info($"CompareLists mismatch - {fieldName}[{i}] differs (legacy='{legacyList[i]}', new='{newList[i]}')"); + return false; + } + } + + return true; + } + + private bool CompareDictionaries(Tracing trace, IDictionary legacyDict, IDictionary newDict, string fieldName) + { + if (legacyDict == null && newDict == null) + { + return true; + } + + if (legacyDict == null || newDict == null) + { + trace.Info($"CompareDictionaries mismatch - {fieldName} - one is null (legacy={legacyDict == null}, new={newDict == null})"); + return false; + } + + if (legacyDict is Dictionary legacyTypedDict && newDict is Dictionary newTypedDict) + { + if (!object.Equals(legacyTypedDict.Comparer, newTypedDict.Comparer)) + { + trace.Info($"CompareDictionaries mismatch - {fieldName} - different comparers (legacy={legacyTypedDict.Comparer.GetType().Name}, new={newTypedDict.Comparer.GetType().Name})"); + return false; + } + } + + if (legacyDict.Count != newDict.Count) + { + trace.Info($"CompareDictionaries mismatch - {fieldName}.Count differs (legacy={legacyDict.Count}, new={newDict.Count})"); + return false; + } + + foreach (var kvp in legacyDict) + { + if (!newDict.TryGetValue(kvp.Key, out var newValue)) + { + trace.Info($"CompareDictionaries mismatch - {fieldName} - key '{kvp.Key}' missing in new result"); + return false; + } + + if (!string.Equals(kvp.Value, newValue, StringComparison.Ordinal)) + { + trace.Info($"CompareDictionaries mismatch - {fieldName}['{kvp.Key}'] differs (legacy='{kvp.Value}', new='{newValue}')"); + return false; + } + } + + return true; + } + + private bool CompareExceptions(Tracing trace, Exception legacyException, Exception newException) + { + if (legacyException == null && newException == null) + { + return true; + } + + if (legacyException == null || newException == null) + { + trace.Info($"CompareExceptions mismatch - one exception is null (legacy={legacyException == null}, new={newException == null})"); + return false; + } + + // Compare exception messages recursively (including inner exceptions) + var legacyMessages = GetExceptionMessages(legacyException); + var newMessages = GetExceptionMessages(newException); + + if (legacyMessages.Count != newMessages.Count) + { + trace.Info($"CompareExceptions mismatch - different number of exception messages (legacy={legacyMessages.Count}, new={newMessages.Count})"); + return false; + } + + for (int i = 0; i < legacyMessages.Count; i++) + { + if (!string.Equals(legacyMessages[i], newMessages[i], StringComparison.Ordinal)) + { + trace.Info($"CompareExceptions mismatch - exception messages differ at level {i} (legacy='{legacyMessages[i]}', new='{newMessages[i]}')"); + return false; + } + } + + return true; + } + + private IList GetExceptionMessages(Exception ex) + { + var trace = HostContext.GetTrace(nameof(ActionManifestManagerWrapper)); + var messages = new List(); + var toProcess = new Queue(); + toProcess.Enqueue(ex); + int count = 0; + + while (toProcess.Count > 0 && count < 50) + { + var current = toProcess.Dequeue(); + if (current == null) continue; + + messages.Add(current.Message); + count++; + + // Special handling for AggregateException - enqueue all inner exceptions + if (current is AggregateException aggregateEx) + { + foreach (var innerEx in aggregateEx.InnerExceptions) + { + if (innerEx != null && count < 50) + { + toProcess.Enqueue(innerEx); + } + } + } + else if (current.InnerException != null) + { + toProcess.Enqueue(current.InnerException); + } + + // Failsafe: if we have too many exceptions, stop and return what we have + if (count >= 50) + { + trace.Info("CompareExceptions failsafe triggered - too many exceptions (50+)"); + break; + } + } + + return messages; + } + } +} diff --git a/src/Runner.Worker/ActionRunner.cs b/src/Runner.Worker/ActionRunner.cs index a17e332b5..da967468a 100644 --- a/src/Runner.Worker/ActionRunner.cs +++ b/src/Runner.Worker/ActionRunner.cs @@ -206,7 +206,7 @@ namespace GitHub.Runner.Worker // Merge the default inputs from the definition if (definition.Data?.Inputs != null) { - var manifestManager = HostContext.GetService(); + var manifestManager = HostContext.GetService(); foreach (var input in definition.Data.Inputs) { string key = input.Key.AssertString("action input name").Value; diff --git a/src/Runner.Worker/ExecutionContext.cs b/src/Runner.Worker/ExecutionContext.cs index daba1a072..5646d46aa 100644 --- a/src/Runner.Worker/ExecutionContext.cs +++ b/src/Runner.Worker/ExecutionContext.cs @@ -1397,7 +1397,7 @@ namespace GitHub.Runner.Worker public static IPipelineTemplateEvaluator ToPipelineTemplateEvaluator(this IExecutionContext context, ObjectTemplating.ITraceWriter traceWriter = null) { // Create wrapper? - if ((context.Global.Variables.GetBoolean(Constants.Runner.Features.CompareTemplateEvaluator) ?? false) || StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_COMPARE_TEMPLATE_EVALUATOR"))) + if ((context.Global.Variables.GetBoolean(Constants.Runner.Features.CompareWorkflowParser) ?? false) || StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_RUNNER_COMPARE_WORKFLOW_PARSER"))) { return (context as ExecutionContext).ToPipelineTemplateEvaluatorInternal(traceWriter); } diff --git a/src/Runner.Worker/GlobalContext.cs b/src/Runner.Worker/GlobalContext.cs index 3caa3567e..da45c010d 100644 --- a/src/Runner.Worker/GlobalContext.cs +++ b/src/Runner.Worker/GlobalContext.cs @@ -30,5 +30,6 @@ namespace GitHub.Runner.Worker public string InfrastructureFailureCategory { get; set; } public JObject ContainerHookState { get; set; } public bool HasTemplateEvaluatorMismatch { get; set; } + public bool HasActionManifestMismatch { get; set; } } } diff --git a/src/Runner.Worker/Handlers/CompositeActionHandler.cs b/src/Runner.Worker/Handlers/CompositeActionHandler.cs index ee4854700..b0fcf8a1e 100644 --- a/src/Runner.Worker/Handlers/CompositeActionHandler.cs +++ b/src/Runner.Worker/Handlers/CompositeActionHandler.cs @@ -187,7 +187,7 @@ namespace GitHub.Runner.Worker.Handlers if (Data.Outputs != null) { // Evaluate the outputs in the steps context to easily retrieve the values - var actionManifestManager = HostContext.GetService(); + var actionManifestManager = HostContext.GetService(); // Format ExpressionValues to Dictionary var evaluateContext = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/src/Runner.Worker/Handlers/ContainerActionHandler.cs b/src/Runner.Worker/Handlers/ContainerActionHandler.cs index b6b080c32..f61344869 100644 --- a/src/Runner.Worker/Handlers/ContainerActionHandler.cs +++ b/src/Runner.Worker/Handlers/ContainerActionHandler.cs @@ -135,7 +135,7 @@ namespace GitHub.Runner.Worker.Handlers var extraExpressionValues = new Dictionary(StringComparer.OrdinalIgnoreCase); extraExpressionValues["inputs"] = inputsContext; - var manifestManager = HostContext.GetService(); + var manifestManager = HostContext.GetService(); if (Data.Arguments != null) { container.ContainerEntryPointArgs = ""; diff --git a/src/Sdk/Sdk.csproj b/src/Sdk/Sdk.csproj index 2b0eed23d..ad7bbd449 100644 --- a/src/Sdk/Sdk.csproj +++ b/src/Sdk/Sdk.csproj @@ -13,6 +13,10 @@ true + + + + diff --git a/src/Test/L0/Worker/ActionManagerL0.cs b/src/Test/L0/Worker/ActionManagerL0.cs index 328c5b5f6..bc4779312 100644 --- a/src/Test/L0/Worker/ActionManagerL0.cs +++ b/src/Test/L0/Worker/ActionManagerL0.cs @@ -2504,14 +2504,20 @@ runs: _pluginManager = new Mock(); _pluginManager.Setup(x => x.GetPluginAction(It.IsAny())).Returns(new RunnerPluginActionInfo() { PluginTypeName = "plugin.class, plugin", PostPluginTypeName = "plugin.cleanup, plugin" }); - var actionManifest = new ActionManifestManager(); - actionManifest.Initialize(_hc); + var actionManifestLegacy = new ActionManifestManagerLegacy(); + actionManifestLegacy.Initialize(_hc); + _hc.SetSingleton(actionManifestLegacy); + var actionManifestNew = new ActionManifestManager(); + actionManifestNew.Initialize(_hc); + _hc.SetSingleton(actionManifestNew); + var actionManifestWrapper = new ActionManifestManagerWrapper(); + actionManifestWrapper.Initialize(_hc); _hc.SetSingleton(_dockerManager.Object); _hc.SetSingleton(_jobServer.Object); _hc.SetSingleton(_launchServer.Object); _hc.SetSingleton(_pluginManager.Object); - _hc.SetSingleton(actionManifest); + _hc.SetSingleton(actionManifestWrapper); _hc.SetSingleton(new HttpClientHandlerFactory()); _configurationStore = new Mock(); diff --git a/src/Test/L0/Worker/ActionManifestManagerL0.cs b/src/Test/L0/Worker/ActionManifestManagerL0.cs index dae75c8f6..b5da3b304 100644 --- a/src/Test/L0/Worker/ActionManifestManagerL0.cs +++ b/src/Test/L0/Worker/ActionManifestManagerL0.cs @@ -1,9 +1,11 @@ -using GitHub.DistributedTask.Expressions2; -using GitHub.DistributedTask.ObjectTemplating.Tokens; -using GitHub.DistributedTask.Pipelines.ContextData; +using GitHub.Actions.Expressions; +using GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens; +using GitHub.Actions.Expressions.Data; using GitHub.DistributedTask.WebApi; using GitHub.Runner.Worker; -using GitHub.Runner.Worker.Expressions; +using GitHub.Actions.WorkflowParser; +using LegacyContextData = GitHub.DistributedTask.Pipelines.ContextData; +using LegacyExpressions = GitHub.DistributedTask.Expressions2; using Moq; using System; using System.Collections.Generic; @@ -49,7 +51,7 @@ namespace GitHub.Runner.Common.Tests.Worker Assert.Equal(ActionExecutionType.Container, result.Execution.ExecutionType); - var containerAction = result.Execution as ContainerActionExecutionData; + var containerAction = result.Execution as ContainerActionExecutionDataNew; Assert.Equal("Dockerfile", containerAction.Image); Assert.Equal("main.sh", containerAction.EntryPoint); @@ -93,7 +95,7 @@ namespace GitHub.Runner.Common.Tests.Worker Assert.Equal(ActionExecutionType.Container, result.Execution.ExecutionType); - var containerAction = result.Execution as ContainerActionExecutionData; + var containerAction = result.Execution as ContainerActionExecutionDataNew; Assert.Equal("Dockerfile", containerAction.Image); Assert.Equal("main.sh", containerAction.EntryPoint); @@ -139,7 +141,7 @@ namespace GitHub.Runner.Common.Tests.Worker Assert.Equal(ActionExecutionType.Container, result.Execution.ExecutionType); - var containerAction = result.Execution as ContainerActionExecutionData; + var containerAction = result.Execution as ContainerActionExecutionDataNew; Assert.Equal("Dockerfile", containerAction.Image); Assert.Equal("main.sh", containerAction.EntryPoint); @@ -185,7 +187,7 @@ namespace GitHub.Runner.Common.Tests.Worker Assert.Equal(ActionExecutionType.Container, result.Execution.ExecutionType); - var containerAction = result.Execution as ContainerActionExecutionData; + var containerAction = result.Execution as ContainerActionExecutionDataNew; Assert.Equal("Dockerfile", containerAction.Image); Assert.Equal("main.sh", containerAction.EntryPoint); @@ -231,7 +233,7 @@ namespace GitHub.Runner.Common.Tests.Worker Assert.Equal(ActionExecutionType.Container, result.Execution.ExecutionType); - var containerAction = result.Execution as ContainerActionExecutionData; + var containerAction = result.Execution as ContainerActionExecutionDataNew; Assert.Equal("Dockerfile", containerAction.Image); Assert.Equal("main.sh", containerAction.EntryPoint); @@ -276,7 +278,7 @@ namespace GitHub.Runner.Common.Tests.Worker Assert.Equal(ActionExecutionType.Container, result.Execution.ExecutionType); - var containerAction = result.Execution as ContainerActionExecutionData; + var containerAction = result.Execution as ContainerActionExecutionDataNew; Assert.Equal("Dockerfile", containerAction.Image); } @@ -314,7 +316,7 @@ namespace GitHub.Runner.Common.Tests.Worker Assert.Equal(ActionExecutionType.Container, result.Execution.ExecutionType); - var containerAction = result.Execution as ContainerActionExecutionData; + var containerAction = result.Execution as ContainerActionExecutionDataNew; Assert.Equal("Dockerfile", containerAction.Image); Assert.Equal("main.sh", containerAction.EntryPoint); @@ -357,7 +359,7 @@ namespace GitHub.Runner.Common.Tests.Worker Assert.Equal(ActionExecutionType.Container, result.Execution.ExecutionType); - var containerAction = result.Execution as ContainerActionExecutionData; + var containerAction = result.Execution as ContainerActionExecutionDataNew; Assert.Equal("docker://ubuntu:18.04", containerAction.Image); Assert.Equal("main.sh", containerAction.EntryPoint); @@ -826,10 +828,10 @@ namespace GitHub.Runner.Common.Tests.Worker arguments.Add(new BasicExpressionToken(null, null, null, "inputs.greeting")); arguments.Add(new StringToken(null, null, null, "test")); - var inputsContext = new DictionaryContextData(); - inputsContext.Add("greeting", new StringContextData("hello")); + var inputsContext = new DictionaryExpressionData(); + inputsContext.Add("greeting", new StringExpressionData("hello")); - var evaluateContext = new Dictionary(StringComparer.OrdinalIgnoreCase); + var evaluateContext = new Dictionary(StringComparer.OrdinalIgnoreCase); evaluateContext["inputs"] = inputsContext; //Act @@ -863,10 +865,10 @@ namespace GitHub.Runner.Common.Tests.Worker environment.Add(new StringToken(null, null, null, "hello"), new BasicExpressionToken(null, null, null, "inputs.greeting")); environment.Add(new StringToken(null, null, null, "test"), new StringToken(null, null, null, "test")); - var inputsContext = new DictionaryContextData(); - inputsContext.Add("greeting", new StringContextData("hello")); + var inputsContext = new DictionaryExpressionData(); + inputsContext.Add("greeting", new StringExpressionData("hello")); - var evaluateContext = new Dictionary(StringComparer.OrdinalIgnoreCase); + var evaluateContext = new Dictionary(StringComparer.OrdinalIgnoreCase); evaluateContext["inputs"] = inputsContext; //Act @@ -896,17 +898,17 @@ namespace GitHub.Runner.Common.Tests.Worker var actionManifest = new ActionManifestManager(); actionManifest.Initialize(_hc); - _ec.Object.ExpressionValues["github"] = new DictionaryContextData + _ec.Object.ExpressionValues["github"] = new LegacyContextData.DictionaryContextData { - { "ref", new StringContextData("refs/heads/main") }, + { "ref", new LegacyContextData.StringContextData("refs/heads/main") }, }; - _ec.Object.ExpressionValues["strategy"] = new DictionaryContextData(); - _ec.Object.ExpressionValues["matrix"] = new DictionaryContextData(); - _ec.Object.ExpressionValues["steps"] = new DictionaryContextData(); - _ec.Object.ExpressionValues["job"] = new DictionaryContextData(); - _ec.Object.ExpressionValues["runner"] = new DictionaryContextData(); - _ec.Object.ExpressionValues["env"] = new DictionaryContextData(); - _ec.Object.ExpressionFunctions.Add(new FunctionInfo("hashFiles", 1, 255)); + _ec.Object.ExpressionValues["strategy"] = new LegacyContextData.DictionaryContextData(); + _ec.Object.ExpressionValues["matrix"] = new LegacyContextData.DictionaryContextData(); + _ec.Object.ExpressionValues["steps"] = new LegacyContextData.DictionaryContextData(); + _ec.Object.ExpressionValues["job"] = new LegacyContextData.DictionaryContextData(); + _ec.Object.ExpressionValues["runner"] = new LegacyContextData.DictionaryContextData(); + _ec.Object.ExpressionValues["env"] = new LegacyContextData.DictionaryContextData(); + _ec.Object.ExpressionFunctions.Add(new LegacyExpressions.FunctionInfo("hashFiles", 1, 255)); //Act var result = actionManifest.EvaluateDefaultInput(_ec.Object, "testInput", new StringToken(null, null, null, "defaultValue")); @@ -934,6 +936,9 @@ namespace GitHub.Runner.Common.Tests.Worker // Test host context. _hc = new TestHostContext(this, name); + var expressionValues = new LegacyContextData.DictionaryContextData(); + var expressionFunctions = new List(); + _ec = new Mock(); _ec.Setup(x => x.Global) .Returns(new GlobalContext @@ -943,8 +948,8 @@ namespace GitHub.Runner.Common.Tests.Worker WriteDebug = true, }); _ec.Setup(x => x.CancellationToken).Returns(_ecTokenSource.Token); - _ec.Setup(x => x.ExpressionValues).Returns(new DictionaryContextData()); - _ec.Setup(x => x.ExpressionFunctions).Returns(new List()); + _ec.Setup(x => x.ExpressionValues).Returns(expressionValues); + _ec.Setup(x => x.ExpressionFunctions).Returns(expressionFunctions); _ec.Setup(x => x.Write(It.IsAny(), It.IsAny())).Callback((string tag, string message) => { _hc.GetTrace().Info($"{tag}{message}"); }); _ec.Setup(x => x.AddIssue(It.IsAny(), It.IsAny())).Callback((Issue issue, ExecutionContextLogOptions logOptions) => { _hc.GetTrace().Info($"[{issue.Type}]{logOptions.LogMessageOverride ?? issue.Message}"); }); } diff --git a/src/Test/L0/Worker/ActionManifestManagerLegacyL0.cs b/src/Test/L0/Worker/ActionManifestManagerLegacyL0.cs new file mode 100644 index 000000000..c11d4b9b6 --- /dev/null +++ b/src/Test/L0/Worker/ActionManifestManagerLegacyL0.cs @@ -0,0 +1,957 @@ +using GitHub.DistributedTask.Expressions2; +using GitHub.DistributedTask.ObjectTemplating.Tokens; +using GitHub.DistributedTask.Pipelines.ContextData; +using GitHub.DistributedTask.WebApi; +using GitHub.Runner.Worker; +using GitHub.Runner.Worker.Expressions; +using Moq; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using Xunit; + +namespace GitHub.Runner.Common.Tests.Worker +{ + public sealed class ActionManifestManagerLegacyL0 + { + private CancellationTokenSource _ecTokenSource; + private Mock _ec; + private TestHostContext _hc; + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_ContainerAction_Dockerfile() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "dockerfileaction.yml")); + + //Assert + + Assert.Equal("Hello World", result.Name); + Assert.Equal("Greet the world and record the time", result.Description); + Assert.Equal(2, result.Inputs.Count); + Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value); + Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value); + Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value); + Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value); + + Assert.Equal(ActionExecutionType.Container, result.Execution.ExecutionType); + + var containerAction = result.Execution as ContainerActionExecutionData; + + Assert.Equal("Dockerfile", containerAction.Image); + Assert.Equal("main.sh", containerAction.EntryPoint); + Assert.Equal("bzz", containerAction.Arguments[0].ToString()); + Assert.Equal("Token", containerAction.Environment[0].Key.ToString()); + Assert.Equal("foo", containerAction.Environment[0].Value.ToString()); + Assert.Equal("Url", containerAction.Environment[1].Key.ToString()); + Assert.Equal("bar", containerAction.Environment[1].Value.ToString()); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_ContainerAction_Dockerfile_Pre() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "dockerfileaction_init.yml")); + + //Assert + + Assert.Equal("Hello World", result.Name); + Assert.Equal("Greet the world and record the time", result.Description); + Assert.Equal(2, result.Inputs.Count); + Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value); + Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value); + Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value); + Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value); + + Assert.Equal(ActionExecutionType.Container, result.Execution.ExecutionType); + + var containerAction = result.Execution as ContainerActionExecutionData; + + Assert.Equal("Dockerfile", containerAction.Image); + Assert.Equal("main.sh", containerAction.EntryPoint); + Assert.Equal("init.sh", containerAction.Pre); + Assert.Equal("success()", containerAction.InitCondition); + Assert.Equal("bzz", containerAction.Arguments[0].ToString()); + Assert.Equal("Token", containerAction.Environment[0].Key.ToString()); + Assert.Equal("foo", containerAction.Environment[0].Value.ToString()); + Assert.Equal("Url", containerAction.Environment[1].Key.ToString()); + Assert.Equal("bar", containerAction.Environment[1].Value.ToString()); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_ContainerAction_Dockerfile_Post() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "dockerfileaction_cleanup.yml")); + + //Assert + + Assert.Equal("Hello World", result.Name); + Assert.Equal("Greet the world and record the time", result.Description); + Assert.Equal(2, result.Inputs.Count); + Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value); + Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value); + Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value); + Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value); + + Assert.Equal(ActionExecutionType.Container, result.Execution.ExecutionType); + + var containerAction = result.Execution as ContainerActionExecutionData; + + Assert.Equal("Dockerfile", containerAction.Image); + Assert.Equal("main.sh", containerAction.EntryPoint); + Assert.Equal("cleanup.sh", containerAction.Post); + Assert.Equal("failure()", containerAction.CleanupCondition); + Assert.Equal("bzz", containerAction.Arguments[0].ToString()); + Assert.Equal("Token", containerAction.Environment[0].Key.ToString()); + Assert.Equal("foo", containerAction.Environment[0].Value.ToString()); + Assert.Equal("Url", containerAction.Environment[1].Key.ToString()); + Assert.Equal("bar", containerAction.Environment[1].Value.ToString()); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_ContainerAction_Dockerfile_Pre_DefaultCondition() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "dockerfileaction_init_default.yml")); + + //Assert + + Assert.Equal("Hello World", result.Name); + Assert.Equal("Greet the world and record the time", result.Description); + Assert.Equal(2, result.Inputs.Count); + Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value); + Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value); + Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value); + Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value); + + Assert.Equal(ActionExecutionType.Container, result.Execution.ExecutionType); + + var containerAction = result.Execution as ContainerActionExecutionData; + + Assert.Equal("Dockerfile", containerAction.Image); + Assert.Equal("main.sh", containerAction.EntryPoint); + Assert.Equal("init.sh", containerAction.Pre); + Assert.Equal("always()", containerAction.InitCondition); + Assert.Equal("bzz", containerAction.Arguments[0].ToString()); + Assert.Equal("Token", containerAction.Environment[0].Key.ToString()); + Assert.Equal("foo", containerAction.Environment[0].Value.ToString()); + Assert.Equal("Url", containerAction.Environment[1].Key.ToString()); + Assert.Equal("bar", containerAction.Environment[1].Value.ToString()); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_ContainerAction_Dockerfile_Post_DefaultCondition() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "dockerfileaction_cleanup_default.yml")); + + //Assert + + Assert.Equal("Hello World", result.Name); + Assert.Equal("Greet the world and record the time", result.Description); + Assert.Equal(2, result.Inputs.Count); + Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value); + Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value); + Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value); + Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value); + + Assert.Equal(ActionExecutionType.Container, result.Execution.ExecutionType); + + var containerAction = result.Execution as ContainerActionExecutionData; + + Assert.Equal("Dockerfile", containerAction.Image); + Assert.Equal("main.sh", containerAction.EntryPoint); + Assert.Equal("cleanup.sh", containerAction.Post); + Assert.Equal("always()", containerAction.CleanupCondition); + Assert.Equal("bzz", containerAction.Arguments[0].ToString()); + Assert.Equal("Token", containerAction.Environment[0].Key.ToString()); + Assert.Equal("foo", containerAction.Environment[0].Value.ToString()); + Assert.Equal("Url", containerAction.Environment[1].Key.ToString()); + Assert.Equal("bar", containerAction.Environment[1].Value.ToString()); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_ContainerAction_NoArgsNoEnv() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "dockerfileaction_noargs_noenv_noentrypoint.yml")); + + //Assert + Assert.Equal("Hello World", result.Name); + Assert.Equal("Greet the world and record the time", result.Description); + Assert.Equal(2, result.Inputs.Count); + Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value); + Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value); + Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value); + Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value); + + Assert.Equal(ActionExecutionType.Container, result.Execution.ExecutionType); + + var containerAction = result.Execution as ContainerActionExecutionData; + + Assert.Equal("Dockerfile", containerAction.Image); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_ContainerAction_Dockerfile_Expression() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "dockerfileaction_arg_env_expression.yml")); + + //Assert + + Assert.Equal("Hello World", result.Name); + Assert.Equal("Greet the world and record the time", result.Description); + Assert.Equal(2, result.Inputs.Count); + Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value); + Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value); + Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value); + Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value); + + Assert.Equal(ActionExecutionType.Container, result.Execution.ExecutionType); + + var containerAction = result.Execution as ContainerActionExecutionData; + + Assert.Equal("Dockerfile", containerAction.Image); + Assert.Equal("main.sh", containerAction.EntryPoint); + Assert.Equal("${{ inputs.greeting }}", containerAction.Arguments[0].ToString()); + Assert.Equal("Token", containerAction.Environment[0].Key.ToString()); + Assert.Equal("foo", containerAction.Environment[0].Value.ToString()); + Assert.Equal("Url", containerAction.Environment[1].Key.ToString()); + Assert.Equal("${{ inputs.entryPoint }}", containerAction.Environment[1].Value.ToString()); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_ContainerAction_DockerHub() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "dockerhubaction.yml")); + + //Assert + Assert.Equal("Hello World", result.Name); + Assert.Equal("Greet the world and record the time", result.Description); + Assert.Equal(2, result.Inputs.Count); + Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value); + Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value); + Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value); + Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value); + + Assert.Equal(ActionExecutionType.Container, result.Execution.ExecutionType); + + var containerAction = result.Execution as ContainerActionExecutionData; + + Assert.Equal("docker://ubuntu:18.04", containerAction.Image); + Assert.Equal("main.sh", containerAction.EntryPoint); + Assert.Equal("bzz", containerAction.Arguments[0].ToString()); + Assert.Equal("Token", containerAction.Environment[0].Key.ToString()); + Assert.Equal("foo", containerAction.Environment[0].Value.ToString()); + Assert.Equal("Url", containerAction.Environment[1].Key.ToString()); + Assert.Equal("bar", containerAction.Environment[1].Value.ToString()); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_NodeAction() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "nodeaction.yml")); + + //Assert + Assert.Equal("Hello World", result.Name); + Assert.Equal("Greet the world and record the time", result.Description); + Assert.Equal(2, result.Inputs.Count); + Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value); + Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value); + Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value); + Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value); + Assert.Equal(1, result.Deprecated.Count); + + Assert.True(result.Deprecated.ContainsKey("greeting")); + result.Deprecated.TryGetValue("greeting", out string value); + Assert.Equal("This property has been deprecated", value); + + Assert.Equal(ActionExecutionType.NodeJS, result.Execution.ExecutionType); + + var nodeAction = result.Execution as NodeJSActionExecutionData; + + Assert.Equal("main.js", nodeAction.Script); + Assert.Equal("node12", nodeAction.NodeVersion); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_Node16Action() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "node16action.yml")); + + //Assert + Assert.Equal("Hello World", result.Name); + Assert.Equal("Greet the world and record the time", result.Description); + Assert.Equal(2, result.Inputs.Count); + Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value); + Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value); + Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value); + Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value); + Assert.Equal(1, result.Deprecated.Count); + + Assert.True(result.Deprecated.ContainsKey("greeting")); + result.Deprecated.TryGetValue("greeting", out string value); + Assert.Equal("This property has been deprecated", value); + + Assert.Equal(ActionExecutionType.NodeJS, result.Execution.ExecutionType); + + var nodeAction = result.Execution as NodeJSActionExecutionData; + + Assert.Equal("main.js", nodeAction.Script); + Assert.Equal("node16", nodeAction.NodeVersion); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_Node20Action() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "node20action.yml")); + + //Assert + Assert.Equal("Hello World", result.Name); + Assert.Equal("Greet the world and record the time", result.Description); + Assert.Equal(2, result.Inputs.Count); + Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value); + Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value); + Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value); + Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value); + Assert.Equal(1, result.Deprecated.Count); + + Assert.True(result.Deprecated.ContainsKey("greeting")); + result.Deprecated.TryGetValue("greeting", out string value); + Assert.Equal("This property has been deprecated", value); + + Assert.Equal(ActionExecutionType.NodeJS, result.Execution.ExecutionType); + + var nodeAction = result.Execution as NodeJSActionExecutionData; + + Assert.Equal("main.js", nodeAction.Script); + Assert.Equal("node20", nodeAction.NodeVersion); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_Node24Action() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "node24action.yml")); + + //Assert + Assert.Equal("Hello World", result.Name); + Assert.Equal("Greet the world and record the time", result.Description); + Assert.Equal(2, result.Inputs.Count); + Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value); + Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value); + Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value); + Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value); + Assert.Equal(1, result.Deprecated.Count); + + Assert.True(result.Deprecated.ContainsKey("greeting")); + result.Deprecated.TryGetValue("greeting", out string value); + Assert.Equal("This property has been deprecated", value); + + Assert.Equal(ActionExecutionType.NodeJS, result.Execution.ExecutionType); + + var nodeAction = result.Execution as NodeJSActionExecutionData; + + Assert.Equal("main.js", nodeAction.Script); + Assert.Equal("node24", nodeAction.NodeVersion); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_NodeAction_Pre() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "nodeaction_init.yml")); + + //Assert + Assert.Equal("Hello World", result.Name); + Assert.Equal("Greet the world and record the time", result.Description); + Assert.Equal(2, result.Inputs.Count); + Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value); + Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value); + Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value); + Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value); + Assert.Equal(1, result.Deprecated.Count); + + Assert.True(result.Deprecated.ContainsKey("greeting")); + result.Deprecated.TryGetValue("greeting", out string value); + Assert.Equal("This property has been deprecated", value); + + Assert.Equal(ActionExecutionType.NodeJS, result.Execution.ExecutionType); + + var nodeAction = result.Execution as NodeJSActionExecutionData; + + Assert.Equal("main.js", nodeAction.Script); + Assert.Equal("init.js", nodeAction.Pre); + Assert.Equal("cancelled()", nodeAction.InitCondition); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_NodeAction_Init_DefaultCondition() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "nodeaction_init_default.yml")); + + //Assert + Assert.Equal("Hello World", result.Name); + Assert.Equal("Greet the world and record the time", result.Description); + Assert.Equal(2, result.Inputs.Count); + Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value); + Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value); + Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value); + Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value); + Assert.Equal(1, result.Deprecated.Count); + + Assert.True(result.Deprecated.ContainsKey("greeting")); + result.Deprecated.TryGetValue("greeting", out string value); + Assert.Equal("This property has been deprecated", value); + + Assert.Equal(ActionExecutionType.NodeJS, result.Execution.ExecutionType); + + var nodeAction = result.Execution as NodeJSActionExecutionData; + + Assert.Equal("main.js", nodeAction.Script); + Assert.Equal("init.js", nodeAction.Pre); + Assert.Equal("always()", nodeAction.InitCondition); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_NodeAction_Cleanup() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "nodeaction_cleanup.yml")); + + //Assert + Assert.Equal("Hello World", result.Name); + Assert.Equal("Greet the world and record the time", result.Description); + Assert.Equal(2, result.Inputs.Count); + Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value); + Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value); + Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value); + Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value); + Assert.Equal(1, result.Deprecated.Count); + + Assert.True(result.Deprecated.ContainsKey("greeting")); + result.Deprecated.TryGetValue("greeting", out string value); + Assert.Equal("This property has been deprecated", value); + + Assert.Equal(ActionExecutionType.NodeJS, result.Execution.ExecutionType); + + var nodeAction = result.Execution as NodeJSActionExecutionData; + + Assert.Equal("main.js", nodeAction.Script); + Assert.Equal("cleanup.js", nodeAction.Post); + Assert.Equal("cancelled()", nodeAction.CleanupCondition); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_NodeAction_Cleanup_DefaultCondition() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "nodeaction_cleanup_default.yml")); + + //Assert + Assert.Equal("Hello World", result.Name); + Assert.Equal("Greet the world and record the time", result.Description); + Assert.Equal(2, result.Inputs.Count); + Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value); + Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value); + Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value); + Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value); + Assert.Equal(1, result.Deprecated.Count); + + Assert.True(result.Deprecated.ContainsKey("greeting")); + result.Deprecated.TryGetValue("greeting", out string value); + Assert.Equal("This property has been deprecated", value); + + Assert.Equal(ActionExecutionType.NodeJS, result.Execution.ExecutionType); + + var nodeAction = result.Execution as NodeJSActionExecutionData; + + Assert.Equal("main.js", nodeAction.Script); + Assert.Equal("cleanup.js", nodeAction.Post); + Assert.Equal("always()", nodeAction.CleanupCondition); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_PluginAction() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "pluginaction.yml")); + + //Assert + Assert.Equal("Hello World", result.Name); + Assert.Equal("Greet the world and record the time", result.Description); + Assert.Equal(2, result.Inputs.Count); + Assert.Equal("greeting", result.Inputs[0].Key.AssertString("key").Value); + Assert.Equal("Hello", result.Inputs[0].Value.AssertString("value").Value); + Assert.Equal("entryPoint", result.Inputs[1].Key.AssertString("key").Value); + Assert.Equal("", result.Inputs[1].Value.AssertString("value").Value); + + Assert.Equal(ActionExecutionType.Plugin, result.Execution.ExecutionType); + + var pluginAction = result.Execution as PluginActionExecutionData; + + Assert.Equal("someplugin", pluginAction.Plugin); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_ConditionalCompositeAction() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + //Act + var result = actionManifest.Load(_ec.Object, Path.Combine(TestUtil.GetTestDataPath(), "conditional_composite_action.yml")); + + //Assert + Assert.Equal("Conditional Composite", result.Name); + Assert.Equal(ActionExecutionType.Composite, result.Execution.ExecutionType); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Load_CompositeActionNoUsing() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + var action_path = Path.Combine(TestUtil.GetTestDataPath(), "composite_action_without_using_token.yml"); + + //Assert + var err = Assert.Throws(() => actionManifest.Load(_ec.Object, action_path)); + Assert.Contains($"Failed to load {action_path}", err.Message); + _ec.Verify(x => x.AddIssue(It.Is(s => s.Message.Contains("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12', 'node16', 'node20' or 'node24'.")), It.IsAny()), Times.Once); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Evaluate_ContainerAction_Args() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + var arguments = new SequenceToken(null, null, null); + arguments.Add(new BasicExpressionToken(null, null, null, "inputs.greeting")); + arguments.Add(new StringToken(null, null, null, "test")); + + var inputsContext = new DictionaryContextData(); + inputsContext.Add("greeting", new StringContextData("hello")); + + var evaluateContext = new Dictionary(StringComparer.OrdinalIgnoreCase); + evaluateContext["inputs"] = inputsContext; + //Act + + var result = actionManifest.EvaluateContainerArguments(_ec.Object, arguments, evaluateContext); + + //Assert + Assert.Equal("hello", result[0]); + Assert.Equal("test", result[1]); + Assert.Equal(2, result.Count); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Evaluate_ContainerAction_Env() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + var environment = new MappingToken(null, null, null); + environment.Add(new StringToken(null, null, null, "hello"), new BasicExpressionToken(null, null, null, "inputs.greeting")); + environment.Add(new StringToken(null, null, null, "test"), new StringToken(null, null, null, "test")); + + var inputsContext = new DictionaryContextData(); + inputsContext.Add("greeting", new StringContextData("hello")); + + var evaluateContext = new Dictionary(StringComparer.OrdinalIgnoreCase); + evaluateContext["inputs"] = inputsContext; + + //Act + var result = actionManifest.EvaluateContainerEnvironment(_ec.Object, environment, evaluateContext); + + //Assert + Assert.Equal("hello", result["hello"]); + Assert.Equal("test", result["test"]); + Assert.Equal(2, result.Count); + } + finally + { + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Evaluate_Default_Input() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManagerLegacy(); + actionManifest.Initialize(_hc); + + _ec.Object.ExpressionValues["github"] = new DictionaryContextData + { + { "ref", new StringContextData("refs/heads/main") }, + }; + _ec.Object.ExpressionValues["strategy"] = new DictionaryContextData(); + _ec.Object.ExpressionValues["matrix"] = new DictionaryContextData(); + _ec.Object.ExpressionValues["steps"] = new DictionaryContextData(); + _ec.Object.ExpressionValues["job"] = new DictionaryContextData(); + _ec.Object.ExpressionValues["runner"] = new DictionaryContextData(); + _ec.Object.ExpressionValues["env"] = new DictionaryContextData(); + _ec.Object.ExpressionFunctions.Add(new FunctionInfo("hashFiles", 1, 255)); + + //Act + var result = actionManifest.EvaluateDefaultInput(_ec.Object, "testInput", new StringToken(null, null, null, "defaultValue")); + + //Assert + Assert.Equal("defaultValue", result); + + //Act + result = actionManifest.EvaluateDefaultInput(_ec.Object, "testInput", new BasicExpressionToken(null, null, null, "github.ref")); + + //Assert + Assert.Equal("refs/heads/main", result); + } + finally + { + Teardown(); + } + } + + private void Setup([CallerMemberName] string name = "") + { + _ecTokenSource?.Dispose(); + _ecTokenSource = new CancellationTokenSource(); + + // Test host context. + _hc = new TestHostContext(this, name); + + _ec = new Mock(); + _ec.Setup(x => x.Global) + .Returns(new GlobalContext + { + FileTable = new List(), + Variables = new Variables(_hc, new Dictionary()), + WriteDebug = true, + }); + _ec.Setup(x => x.CancellationToken).Returns(_ecTokenSource.Token); + _ec.Setup(x => x.ExpressionValues).Returns(new DictionaryContextData()); + _ec.Setup(x => x.ExpressionFunctions).Returns(new List()); + _ec.Setup(x => x.Write(It.IsAny(), It.IsAny())).Callback((string tag, string message) => { _hc.GetTrace().Info($"{tag}{message}"); }); + _ec.Setup(x => x.AddIssue(It.IsAny(), It.IsAny())).Callback((Issue issue, ExecutionContextLogOptions logOptions) => { _hc.GetTrace().Info($"[{issue.Type}]{logOptions.LogMessageOverride ?? issue.Message}"); }); + } + + private void Teardown() + { + _hc?.Dispose(); + } + } +} diff --git a/src/Test/L0/Worker/ActionRunnerL0.cs b/src/Test/L0/Worker/ActionRunnerL0.cs index 30842c99b..8186e25b6 100644 --- a/src/Test/L0/Worker/ActionRunnerL0.cs +++ b/src/Test/L0/Worker/ActionRunnerL0.cs @@ -25,7 +25,7 @@ namespace GitHub.Runner.Common.Tests.Worker private Mock _ec; private TestHostContext _hc; private ActionRunner _actionRunner; - private IActionManifestManager _actionManifestManager; + private IActionManifestManagerWrapper _actionManifestManager; private Mock _fileCommandManager; private DictionaryContextData _context = new(); @@ -459,9 +459,16 @@ namespace GitHub.Runner.Common.Tests.Worker _handlerFactory = new Mock(); _defaultStepHost = new Mock(); - _actionManifestManager = new ActionManifestManager(); - _fileCommandManager = new Mock(); + + var actionManifestLegacy = new ActionManifestManagerLegacy(); + actionManifestLegacy.Initialize(_hc); + _hc.SetSingleton(actionManifestLegacy); + var actionManifestNew = new ActionManifestManager(); + actionManifestNew.Initialize(_hc); + _hc.SetSingleton(actionManifestNew); + _actionManifestManager = new ActionManifestManagerWrapper(); _actionManifestManager.Initialize(_hc); + _fileCommandManager = new Mock(); var githubContext = new GitHubContext(); githubContext.Add("event", JToken.Parse("{\"foo\":\"bar\"}").ToPipelineContextData()); @@ -489,7 +496,7 @@ namespace GitHub.Runner.Common.Tests.Worker _hc.SetSingleton(_actionManager.Object); _hc.SetSingleton(_handlerFactory.Object); - _hc.SetSingleton(_actionManifestManager); + _hc.SetSingleton(_actionManifestManager); _hc.EnqueueInstance(_defaultStepHost.Object);