diff --git a/src/Runner.Worker/ActionCommandManager.cs b/src/Runner.Worker/ActionCommandManager.cs index ed9ea3d57f7..a2819fdccb7 100644 --- a/src/Runner.Worker/ActionCommandManager.cs +++ b/src/Runner.Worker/ActionCommandManager.cs @@ -191,6 +191,7 @@ public void ProcessCommand(IExecutionContext context, string line, ActionCommand } context.EnvironmentVariables[envName] = command.Data; + context.SetEnvContext(envName, command.Data); context.Output(line); context.Debug($"{envName}='{command.Data}'"); omitEcho = true; diff --git a/src/Runner.Worker/ActionManager.cs b/src/Runner.Worker/ActionManager.cs index 87f4e3ca88f..e92aaf4f50c 100644 --- a/src/Runner.Worker/ActionManager.cs +++ b/src/Runner.Worker/ActionManager.cs @@ -797,9 +797,17 @@ public sealed class ScriptActionExecutionData : ActionExecutionData public abstract class ActionExecutionData { + private string _cleanupCondition = $"{Constants.Expressions.Always}()"; + public abstract ActionExecutionType ExecutionType { get; } public abstract bool HasCleanup { get; } + + public string CleanupCondition + { + get { return _cleanupCondition; } + set { _cleanupCondition = value; } + } } public class ContainerSetupInfo diff --git a/src/Runner.Worker/ActionManifestManager.cs b/src/Runner.Worker/ActionManifestManager.cs index fd17b2e8594..210e32f866a 100644 --- a/src/Runner.Worker/ActionManifestManager.cs +++ b/src/Runner.Worker/ActionManifestManager.cs @@ -25,6 +25,8 @@ public interface IActionManifestManager : IRunnerService List EvaluateContainerArguments(IExecutionContext executionContext, SequenceToken token, IDictionary contextData); Dictionary EvaluateContainerEnvironment(IExecutionContext executionContext, MappingToken token, IDictionary contextData); + + string EvaluateDefaultInput(IExecutionContext executionContext, string inputName, TemplateToken token, IDictionary contextData); } public sealed class ActionManifestManager : RunnerService, IActionManifestManager @@ -207,6 +209,38 @@ public Dictionary EvaluateContainerEnvironment( return result; } + public string EvaluateDefaultInput( + IExecutionContext executionContext, + string inputName, + TemplateToken token, + IDictionary contextData) + { + string result = ""; + if (token != null) + { + var context = CreateContext(executionContext, contextData); + try + { + var evaluateResult = TemplateEvaluator.Evaluate(context, "input-default-context", token, 0, null, omitHeader: true); + context.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); + context.Errors.Add(ex); + } + + context.Errors.Check(); + } + + return result; + } + private TemplateContext CreateContext( IExecutionContext executionContext, IDictionary contextData) @@ -248,6 +282,7 @@ private ActionExecutionData ConvertRuns( var pluginToken = default(StringToken); var postToken = default(StringToken); var postEntrypointToken = default(StringToken); + var postIfToken = default(StringToken); foreach (var run in runsMapping) { var runsKey = run.Key.AssertString("runs key").Value; @@ -280,6 +315,9 @@ private ActionExecutionData ConvertRuns( case "post-entrypoint": postEntrypointToken = run.Value.AssertString("post-entrypoint"); break; + case "post-if": + postIfToken = run.Value.AssertString("post-if"); + break; default: Trace.Info($"Ignore run property {runsKey}."); break; @@ -302,7 +340,8 @@ private ActionExecutionData ConvertRuns( Arguments = argsToken, EntryPoint = entrypointToken?.Value, Environment = envToken, - Cleanup = postEntrypointToken?.Value + Cleanup = postEntrypointToken?.Value, + CleanupCondition = postIfToken?.Value }; } } @@ -317,7 +356,8 @@ private ActionExecutionData ConvertRuns( return new NodeJSActionExecutionData() { Script = mainToken.Value, - Cleanup = postToken?.Value + Cleanup = postToken?.Value, + CleanupCondition = postIfToken?.Value }; } } @@ -355,8 +395,7 @@ private void ConvertInputs( if (string.Equals(metadataName, "default", StringComparison.OrdinalIgnoreCase)) { hasDefault = true; - var inputDefault = metadata.Value.AssertString("input default"); - actionDefinition.Inputs.Add(inputName, inputDefault); + actionDefinition.Inputs.Add(inputName, metadata.Value); } else if (string.Equals(metadataName, "deprecationMessage", StringComparison.OrdinalIgnoreCase)) { diff --git a/src/Runner.Worker/ActionRunner.cs b/src/Runner.Worker/ActionRunner.cs index 82d0cc740c0..0ec510c66c8 100644 --- a/src/Runner.Worker/ActionRunner.cs +++ b/src/Runner.Worker/ActionRunner.cs @@ -94,7 +94,7 @@ public async Task RunAsync() { postDisplayName = $"Post {this.DisplayName}"; } - ExecutionContext.RegisterPostJobAction(postDisplayName, Action); + ExecutionContext.RegisterPostJobAction(postDisplayName, handlerData.CleanupCondition, Action); } IStepHost stepHost = HostContext.CreateService(); @@ -144,13 +144,19 @@ public async Task RunAsync() // Merge the default inputs from the definition if (definition.Data?.Inputs != null) { + var manifestManager = HostContext.GetService(); foreach (var input in (definition.Data?.Inputs)) { string key = input.Key.AssertString("action input name").Value; - string value = input.Value.AssertString("action input default value").Value; if (!inputs.ContainsKey(key)) { - inputs[key] = value; + var evaluateContext = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var data in ExecutionContext.ExpressionValues) + { + evaluateContext[data.Key] = data.Value; + } + + inputs[key] = manifestManager.EvaluateDefaultInput(ExecutionContext, key, input.Value, evaluateContext); } } } diff --git a/src/Runner.Worker/ExecutionContext.cs b/src/Runner.Worker/ExecutionContext.cs index 81946ce7d1a..2da42fa867c 100644 --- a/src/Runner.Worker/ExecutionContext.cs +++ b/src/Runner.Worker/ExecutionContext.cs @@ -19,6 +19,7 @@ using GitHub.Runner.Sdk; using Newtonsoft.Json; using System.Text; +using System.Collections; namespace GitHub.Runner.Worker { @@ -74,7 +75,7 @@ public interface IExecutionContext : IRunnerService // timeline record update methods void Start(string currentOperation = null); TaskResult Complete(TaskResult? result = null, string currentOperation = null, string resultCode = null); - string GetRunnerContext(string name); + void SetEnvContext(string name, string value); void SetRunnerContext(string name, string value); string GetGitHubContext(string name); void SetGitHubContext(string name, string value); @@ -95,7 +96,7 @@ public interface IExecutionContext : IRunnerService // others void ForceTaskComplete(); - void RegisterPostJobAction(string displayName, Pipelines.ActionStep action); + void RegisterPostJobAction(string displayName, string condition, Pipelines.ActionStep action); } public sealed class ExecutionContext : RunnerService, IExecutionContext @@ -235,7 +236,7 @@ public void ForceTaskComplete() }); } - public void RegisterPostJobAction(string displayName, Pipelines.ActionStep action) + public void RegisterPostJobAction(string displayName, string condition, Pipelines.ActionStep action) { if (action.Reference.Type != ActionSourceType.Repository) { @@ -252,7 +253,7 @@ public void RegisterPostJobAction(string displayName, Pipelines.ActionStep actio var actionRunner = HostContext.CreateService(); actionRunner.Action = action; actionRunner.Stage = ActionRunStage.Post; - actionRunner.Condition = $"{Constants.Expressions.Always}()"; + actionRunner.Condition = condition; actionRunner.DisplayName = displayName; actionRunner.ExecutionContext = Root.CreatePostChild(displayName, $"{actionRunner.Action.Name}_post", IntraActionState); Root.PostJobSteps.Push(actionRunner); @@ -366,18 +367,18 @@ public void SetRunnerContext(string name, string value) runnerContext[name] = new StringContextData(value); } - public string GetRunnerContext(string name) + public void SetEnvContext(string name, string value) { ArgUtil.NotNullOrEmpty(name, nameof(name)); - var runnerContext = ExpressionValues["runner"] as RunnerContext; - if (runnerContext.TryGetValue(name, out var value)) - { - return value as StringContextData; - } - else - { - return null; - } + +#if OS_WINDOWS + var envContext = ExpressionValues["env"] as DictionaryContextData; + envContext[name] = new StringContextData(value); +#else + var envContext = ExpressionValues["env"] as CaseSensitiveDictionaryContextData; + envContext[name] = new StringContextData(value); +#endif + } public void SetGitHubContext(string name, string value) @@ -611,6 +612,14 @@ public void InitializeJob(Pipelines.AgentJobRequestMessage message, Cancellation } ExpressionValues["github"] = githubContext; + Trace.Info("Initialize Env context"); +#if OS_WINDOWS + ExpressionValues["env"] = new DictionaryContextData(); +#else + + ExpressionValues["env"] = new CaseSensitiveDictionaryContextData(); +#endif + // Prepend Path PrependPath = new List(); diff --git a/src/Runner.Worker/Handlers/ContainerActionHandler.cs b/src/Runner.Worker/Handlers/ContainerActionHandler.cs index c322e1b90e4..53be0ce27c1 100644 --- a/src/Runner.Worker/Handlers/ContainerActionHandler.cs +++ b/src/Runner.Worker/Handlers/ContainerActionHandler.cs @@ -8,6 +8,7 @@ using GitHub.Runner.Sdk; using GitHub.DistributedTask.WebApi; using GitHub.DistributedTask.Pipelines.ContextData; +using System.Linq; namespace GitHub.Runner.Worker.Handlers { @@ -172,6 +173,15 @@ public async Task RunAsync(ActionRunStage stage) } } + // Add Actions Runtime server info + var systemConnection = ExecutionContext.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase)); + Environment["ACTIONS_RUNTIME_URL"] = systemConnection.Url.AbsoluteUri; + Environment["ACTIONS_RUNTIME_TOKEN"] = systemConnection.Authorization.Parameters[EndpointAuthorizationParameters.AccessToken]; + if (systemConnection.Data.TryGetValue("CacheServerUrl", out var cacheUrl) && !string.IsNullOrEmpty(cacheUrl)) + { + Environment["ACTIONS_CACHE_URL"] = cacheUrl; + } + foreach (var variable in this.Environment) { container.ContainerEnvironmentVariables[variable.Key] = container.TranslateToContainerPath(variable.Value); diff --git a/src/Runner.Worker/Handlers/NodeScriptActionHandler.cs b/src/Runner.Worker/Handlers/NodeScriptActionHandler.cs index 8ce516facab..911265e3c75 100644 --- a/src/Runner.Worker/Handlers/NodeScriptActionHandler.cs +++ b/src/Runner.Worker/Handlers/NodeScriptActionHandler.cs @@ -6,6 +6,7 @@ using GitHub.DistributedTask.WebApi; using Pipelines = GitHub.DistributedTask.Pipelines; using System; +using System.Linq; namespace GitHub.Runner.Worker.Handlers { @@ -44,6 +45,15 @@ public async Task RunAsync(ActionRunStage stage) } } + // Add Actions Runtime server info + var systemConnection = ExecutionContext.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase)); + Environment["ACTIONS_RUNTIME_URL"] = systemConnection.Url.AbsoluteUri; + Environment["ACTIONS_RUNTIME_TOKEN"] = systemConnection.Authorization.Parameters[EndpointAuthorizationParameters.AccessToken]; + if (systemConnection.Data.TryGetValue("CacheServerUrl", out var cacheUrl) && !string.IsNullOrEmpty(cacheUrl)) + { + Environment["ACTIONS_CACHE_URL"] = cacheUrl; + } + // Resolve the target script. string target = null; if (stage == ActionRunStage.Main) diff --git a/src/Runner.Worker/JobExtension.cs b/src/Runner.Worker/JobExtension.cs index 0bb4fca5ffb..d57e1c20108 100644 --- a/src/Runner.Worker/JobExtension.cs +++ b/src/Runner.Worker/JobExtension.cs @@ -85,6 +85,7 @@ public async Task> InitializeJob(IExecutionContext jobContext, Pipel foreach (var pair in environmentVariables) { context.EnvironmentVariables[pair.Key] = pair.Value ?? string.Empty; + context.SetEnvContext(pair.Key, pair.Value ?? string.Empty); } } diff --git a/src/Runner.Worker/action_yaml.json b/src/Runner.Worker/action_yaml.json index f45b89152f1..a30de160674 100644 --- a/src/Runner.Worker/action_yaml.json +++ b/src/Runner.Worker/action_yaml.json @@ -22,7 +22,7 @@ "input": { "mapping": { "properties": { - "default": "string" + "default": "input-default-context" }, "loose-key-type": "non-empty-string", "loose-value-type": "any" @@ -43,7 +43,8 @@ "entrypoint": "non-empty-string", "args": "container-runs-args", "env": "container-runs-env", - "post-entrypoint": "non-empty-string" + "post-entrypoint": "non-empty-string", + "post-if": "non-empty-string" } } }, @@ -66,7 +67,8 @@ "properties": { "using": "non-empty-string", "main": "non-empty-string", - "post": "non-empty-string" + "post": "non-empty-string", + "post-if": "non-empty-string" } } }, @@ -83,6 +85,18 @@ ], "string": {} }, + "input-default-context": { + "context": [ + "github", + "strategy", + "matrix", + "steps", + "job", + "runner", + "env" + ], + "string": {} + }, "non-empty-string": { "string": { "require-non-empty": true diff --git a/src/Sdk/DTExpressions2/Expressions2/ExpressionConstants.cs b/src/Sdk/DTExpressions2/Expressions2/ExpressionConstants.cs index 1cc38288ac1..5a26d50e459 100644 --- a/src/Sdk/DTExpressions2/Expressions2/ExpressionConstants.cs +++ b/src/Sdk/DTExpressions2/Expressions2/ExpressionConstants.cs @@ -15,6 +15,7 @@ static ExpressionConstants() AddFunction("join", 1, 2); AddFunction("startsWith", 2, 2); AddFunction("toJson", 1, 1); + AddFunction("hashFiles", 1, 1); } private static void AddFunction(String name, Int32 minParameters, Int32 maxParameters) diff --git a/src/Sdk/DTExpressions2/Expressions2/Sdk/Functions/HashFiles.cs b/src/Sdk/DTExpressions2/Expressions2/Sdk/Functions/HashFiles.cs new file mode 100644 index 00000000000..f3a9e941aa2 --- /dev/null +++ b/src/Sdk/DTExpressions2/Expressions2/Sdk/Functions/HashFiles.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Minimatch; +using System.IO; +using System.Security.Cryptography; +using GitHub.DistributedTask.Pipelines.ContextData; +using GitHub.DistributedTask.Pipelines.ObjectTemplating; +namespace GitHub.DistributedTask.Expressions2.Sdk.Functions +{ + internal sealed class HashFiles : Function + { + protected sealed override Object EvaluateCore( + EvaluationContext context, + out ResultMemory resultMemory) + { + resultMemory = null; + + // hashFiles() only works on the runner and only works with files under GITHUB_WORKSPACE + // Since GITHUB_WORKSPACE is set by runner, I am using that as the fact of this code runs on server or runner. + if (context.State is ObjectTemplating.TemplateContext templateContext && + templateContext.ExpressionValues.TryGetValue(PipelineTemplateConstants.GitHub, out var githubContextData) && + githubContextData is DictionaryContextData githubContext && + githubContext.TryGetValue(PipelineTemplateConstants.Workspace, out var workspace) == true && + workspace is StringContextData workspaceData) + { + string searchRoot = workspaceData.Value; + string pattern = Parameters[0].Evaluate(context).ConvertToString(); + + context.Trace.Info($"Search root directory: '{searchRoot}'"); + context.Trace.Info($"Search pattern: '{pattern}'"); + var files = Directory.GetFiles(searchRoot, "*", SearchOption.AllDirectories).OrderBy(x => x).ToList(); + if (files.Count == 0) + { + throw new ArgumentException($"'hashFiles({pattern})' failed. Directory '{searchRoot}' is empty"); + } + else + { + context.Trace.Info($"Found {files.Count} files"); + } + + var matcher = new Minimatcher(pattern, s_minimatchOptions); + files = matcher.Filter(files).ToList(); + if (files.Count == 0) + { + throw new ArgumentException($"'hashFiles({pattern})' failed. Search pattern '{pattern}' doesn't match any file under '{searchRoot}'"); + } + else + { + context.Trace.Info($"{files.Count} matches to hash"); + } + + List filesSha256 = new List(); + foreach (var file in files) + { + context.Trace.Info($"Hash {file}"); + using (SHA256 sha256hash = SHA256.Create()) + { + using (var fileStream = File.OpenRead(file)) + { + filesSha256.AddRange(sha256hash.ComputeHash(fileStream)); + } + } + } + + using (SHA256 sha256hash = SHA256.Create()) + { + var hashBytes = sha256hash.ComputeHash(filesSha256.ToArray()); + StringBuilder hashString = new StringBuilder(); + for (int i = 0; i < hashBytes.Length; i++) + { + hashString.Append(hashBytes[i].ToString("x2")); + } + var result = hashString.ToString(); + context.Trace.Info($"Final hash result: '{result}'"); + return result; + } + } + else + { + throw new InvalidOperationException("'hashfiles' expression function is only supported under runner context."); + } + } + + private static readonly Options s_minimatchOptions = new Options + { + Dot = true, + NoBrace = true, + NoCase = Environment.OSVersion.Platform != PlatformID.Unix && Environment.OSVersion.Platform != PlatformID.MacOSX + }; + } +} \ No newline at end of file diff --git a/src/Sdk/DTPipelines/Pipelines/ContextData/CaseSensitiveDictionaryContextData.cs b/src/Sdk/DTPipelines/Pipelines/ContextData/CaseSensitiveDictionaryContextData.cs new file mode 100644 index 00000000000..a1fa19ec71e --- /dev/null +++ b/src/Sdk/DTPipelines/Pipelines/ContextData/CaseSensitiveDictionaryContextData.cs @@ -0,0 +1,293 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.Serialization; +using GitHub.DistributedTask.Expressions2.Sdk; +using GitHub.Services.WebApi.Internal; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace GitHub.DistributedTask.Pipelines.ContextData +{ + [DataContract] + [JsonObject] + [ClientIgnore] + [EditorBrowsable(EditorBrowsableState.Never)] + public class CaseSensitiveDictionaryContextData : PipelineContextData, IEnumerable>, IReadOnlyObject + { + public CaseSensitiveDictionaryContextData() + : base(PipelineContextDataType.CaseSensitiveDictionary) + { + } + + [IgnoreDataMember] + public Int32 Count => m_list?.Count ?? 0; + + [IgnoreDataMember] + public IEnumerable Keys + { + get + { + if (m_list?.Count > 0) + { + foreach (var pair in m_list) + { + yield return pair.Key; + } + } + } + } + + [IgnoreDataMember] + public IEnumerable Values + { + get + { + if (m_list?.Count > 0) + { + foreach (var pair in m_list) + { + yield return pair.Value; + } + } + } + } + + IEnumerable IReadOnlyObject.Values + { + get + { + if (m_list?.Count > 0) + { + foreach (var pair in m_list) + { + yield return pair.Value; + } + } + } + } + + private Dictionary IndexLookup + { + get + { + if (m_indexLookup == null) + { + m_indexLookup = new Dictionary(StringComparer.Ordinal); + if (m_list?.Count > 0) + { + for (var i = 0; i < m_list.Count; i++) + { + var pair = m_list[i]; + m_indexLookup.Add(pair.Key, i); + } + } + } + + return m_indexLookup; + } + } + + private List List + { + get + { + if (m_list == null) + { + m_list = new List(); + } + + return m_list; + } + } + + public PipelineContextData this[String key] + { + get + { + var index = IndexLookup[key]; + return m_list[index].Value; + } + + set + { + // Existing + if (IndexLookup.TryGetValue(key, out var index)) + { + key = m_list[index].Key; // preserve casing + m_list[index] = new DictionaryContextDataPair(key, value); + } + // New + else + { + Add(key, value); + } + } + } + + Object IReadOnlyObject.this[String key] + { + get + { + var index = IndexLookup[key]; + return m_list[index].Value; + } + } + + internal KeyValuePair this[Int32 index] + { + get + { + var pair = m_list[index]; + return new KeyValuePair(pair.Key, pair.Value); + } + } + + public void Add(IEnumerable> pairs) + { + foreach (var pair in pairs) + { + Add(pair.Key, pair.Value); + } + } + + public void Add( + String key, + PipelineContextData value) + { + IndexLookup.Add(key, m_list?.Count ?? 0); + List.Add(new DictionaryContextDataPair(key, value)); + } + + public override PipelineContextData Clone() + { + var result = new CaseSensitiveDictionaryContextData(); + + if (m_list?.Count > 0) + { + result.m_list = new List(m_list.Count); + foreach (var item in m_list) + { + result.m_list.Add(new DictionaryContextDataPair(item.Key, item.Value?.Clone())); + } + } + + return result; + } + + public override JToken ToJToken() + { + var json = new JObject(); + if (m_list?.Count > 0) + { + foreach (var item in m_list) + { + json.Add(item.Key, item.Value?.ToJToken() ?? JValue.CreateNull()); + } + } + return json; + } + + public Boolean ContainsKey(String key) + { + return TryGetValue(key, out _); + } + + public IEnumerator> GetEnumerator() + { + if (m_list?.Count > 0) + { + foreach (var pair in m_list) + { + yield return new KeyValuePair(pair.Key, pair.Value); + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + if (m_list?.Count > 0) + { + foreach (var pair in m_list) + { + yield return new KeyValuePair(pair.Key, pair.Value); + } + } + } + + IEnumerator IReadOnlyObject.GetEnumerator() + { + if (m_list?.Count > 0) + { + foreach (var pair in m_list) + { + yield return new KeyValuePair(pair.Key, pair.Value); + } + } + } + + public Boolean TryGetValue( + String key, + out PipelineContextData value) + { + if (m_list?.Count > 0 && + IndexLookup.TryGetValue(key, out var index)) + { + value = m_list[index].Value; + return true; + } + + value = null; + return false; + } + + Boolean IReadOnlyObject.TryGetValue( + String key, + out Object value) + { + if (TryGetValue(key, out PipelineContextData data)) + { + value = data; + return true; + } + + value = null; + return false; + } + + [OnSerializing] + private void OnSerializing(StreamingContext context) + { + if (m_list?.Count == 0) + { + m_list = null; + } + } + + [DataContract] + [ClientIgnore] + [EditorBrowsable(EditorBrowsableState.Never)] + private sealed class DictionaryContextDataPair + { + public DictionaryContextDataPair( + String key, + PipelineContextData value) + { + Key = key; + Value = value; + } + + [DataMember(Name = "k")] + public readonly String Key; + + [DataMember(Name = "v")] + public readonly PipelineContextData Value; + } + + private Dictionary m_indexLookup; + + [DataMember(Name = "d", EmitDefaultValue = false)] + private List m_list; + } +} diff --git a/src/Sdk/DTPipelines/Pipelines/ContextData/PipelineContextDataJsonConverter.cs b/src/Sdk/DTPipelines/Pipelines/ContextData/PipelineContextDataJsonConverter.cs index c09b905c100..ce861953503 100644 --- a/src/Sdk/DTPipelines/Pipelines/ContextData/PipelineContextDataJsonConverter.cs +++ b/src/Sdk/DTPipelines/Pipelines/ContextData/PipelineContextDataJsonConverter.cs @@ -89,6 +89,10 @@ public override Object ReadJson( newValue = new NumberContextData(0); break; + case PipelineContextDataType.CaseSensitiveDictionary: + newValue = new CaseSensitiveDictionaryContextData(); + break; + default: throw new NotSupportedException($"Unexpected {nameof(PipelineContextDataType)} '{type}'"); } @@ -165,6 +169,28 @@ public override void WriteJson( } writer.WriteEndObject(); } + else if (value is CaseSensitiveDictionaryContextData caseSensitiveDictionaryData) + { + writer.WriteStartObject(); + writer.WritePropertyName("t"); + writer.WriteValue(PipelineContextDataType.CaseSensitiveDictionary); + if (caseSensitiveDictionaryData.Count > 0) + { + writer.WritePropertyName("d"); + writer.WriteStartArray(); + foreach (var pair in caseSensitiveDictionaryData) + { + writer.WriteStartObject(); + writer.WritePropertyName("k"); + writer.WriteValue(pair.Key); + writer.WritePropertyName("v"); + serializer.Serialize(writer, pair.Value); + writer.WriteEndObject(); + } + writer.WriteEndArray(); + } + writer.WriteEndObject(); + } else { throw new NotSupportedException($"Unexpected type '{value.GetType().Name}'"); diff --git a/src/Sdk/DTPipelines/Pipelines/ContextData/PipelineContextDataType.cs b/src/Sdk/DTPipelines/Pipelines/ContextData/PipelineContextDataType.cs index f8f114f1026..0053ff5e045 100644 --- a/src/Sdk/DTPipelines/Pipelines/ContextData/PipelineContextDataType.cs +++ b/src/Sdk/DTPipelines/Pipelines/ContextData/PipelineContextDataType.cs @@ -13,5 +13,7 @@ internal static class PipelineContextDataType internal const Int32 Boolean = 3; internal const Int32 Number = 4; + + internal const Int32 CaseSensitiveDictionary = 5; } } diff --git a/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConstants.cs b/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConstants.cs index deb10d706e0..0675b993c9a 100644 --- a/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConstants.cs +++ b/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConstants.cs @@ -11,7 +11,7 @@ public sealed class PipelineTemplateConstants public const String CancelTimeoutMinutes = "cancel-timeout-minutes"; public const String Cancelled = "cancelled"; public const String Checkout = "checkout"; - public const String Clean= "clean"; + public const String Clean = "clean"; public const String Container = "container"; public const String ContinueOnError = "continue-on-error"; public const String Env = "env"; @@ -77,5 +77,6 @@ public sealed class PipelineTemplateConstants public const String Workflow_1_0 = "workflow-v1.0"; public const String WorkflowRoot = "workflow-root"; public const String WorkingDirectory = "working-directory"; + public const String Workspace = "workspace"; } } diff --git a/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConverter.cs b/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConverter.cs index 116de20d92a..45f6dc697ae 100644 --- a/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConverter.cs +++ b/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConverter.cs @@ -1123,6 +1123,7 @@ private static String ConvertToIfCondition( new NamedValueInfo(PipelineTemplateConstants.GitHub), new NamedValueInfo(PipelineTemplateConstants.Job), new NamedValueInfo(PipelineTemplateConstants.Runner), + new NamedValueInfo(PipelineTemplateConstants.Env), }; private static readonly INamedValueInfo[] s_stepInTemplateNamedValues = new INamedValueInfo[] { @@ -1133,6 +1134,7 @@ private static String ConvertToIfCondition( new NamedValueInfo(PipelineTemplateConstants.GitHub), new NamedValueInfo(PipelineTemplateConstants.Job), new NamedValueInfo(PipelineTemplateConstants.Runner), + new NamedValueInfo(PipelineTemplateConstants.Env), }; private static readonly IFunctionInfo[] s_stepConditionFunctions = new IFunctionInfo[] { diff --git a/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateEvaluator.cs b/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateEvaluator.cs index f60df8699f5..fc69ca272fb 100644 --- a/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateEvaluator.cs +++ b/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateEvaluator.cs @@ -520,6 +520,7 @@ private TemplateContext CreateContext(DictionaryContextData contextData) PipelineTemplateConstants.Inputs, PipelineTemplateConstants.Job, PipelineTemplateConstants.Runner, + PipelineTemplateConstants.Env, }; } } diff --git a/src/Sdk/DTPipelines/workflow-v1.0.json b/src/Sdk/DTPipelines/workflow-v1.0.json index 06e5935463a..02741c871c5 100644 --- a/src/Sdk/DTPipelines/workflow-v1.0.json +++ b/src/Sdk/DTPipelines/workflow-v1.0.json @@ -43,7 +43,8 @@ "steps", "inputs", "job", - "runner" + "runner", + "env" ], "one-of": [ "string", @@ -69,7 +70,8 @@ "steps", "inputs", "job", - "runner" + "runner", + "env" ], "string": {} }, @@ -113,7 +115,8 @@ "secrets", "steps", "job", - "runner" + "runner", + "env" ], "string": {} }, @@ -383,7 +386,8 @@ "secrets", "steps", "job", - "runner" + "runner", + "env" ], "mapping": { "loose-key-type": "non-empty-string", @@ -400,7 +404,8 @@ "steps", "inputs", "job", - "runner" + "runner", + "env" ], "mapping": { "loose-key-type": "non-empty-string", @@ -415,9 +420,9 @@ "matrix", "secrets", "steps", - "github", "job", - "runner" + "runner", + "env" ], "mapping": { "loose-key-type": "non-empty-string", @@ -434,7 +439,8 @@ "steps", "inputs", "job", - "runner" + "runner", + "env" ], "mapping": { "loose-key-type": "non-empty-string", @@ -449,9 +455,9 @@ "matrix", "secrets", "steps", - "github", "job", - "runner" + "runner", + "env" ], "mapping": { "loose-key-type": "non-empty-string", @@ -511,7 +517,8 @@ "steps", "inputs", "job", - "runner" + "runner", + "env" ], "mapping": { "loose-key-type": "non-empty-string", @@ -556,9 +563,9 @@ "matrix", "secrets", "steps", - "github", "job", - "runner" + "runner", + "env" ], "boolean": {} }, @@ -570,10 +577,10 @@ "matrix", "secrets", "steps", - "github", "inputs", "job", - "runner" + "runner", + "env" ], "boolean": {} }, @@ -585,9 +592,9 @@ "matrix", "secrets", "steps", - "github", "job", - "runner" + "runner", + "env" ], "number": {} }, @@ -599,10 +606,10 @@ "matrix", "secrets", "steps", - "github", "inputs", "job", - "runner" + "runner", + "env" ], "number": {} }, @@ -614,9 +621,9 @@ "matrix", "secrets", "steps", - "github", "job", - "runner" + "runner", + "env" ], "string": {} }, @@ -630,7 +637,8 @@ "steps", "inputs", "job", - "runner" + "runner", + "env" ], "string": {} } diff --git a/src/Sdk/Sdk.csproj b/src/Sdk/Sdk.csproj index 0ecf65dcace..13323616ce4 100644 --- a/src/Sdk/Sdk.csproj +++ b/src/Sdk/Sdk.csproj @@ -26,11 +26,7 @@ - - - - diff --git a/src/Test/L0/Worker/ActionManifestManagerL0.cs b/src/Test/L0/Worker/ActionManifestManagerL0.cs index a7c1949a115..821e969f751 100644 --- a/src/Test/L0/Worker/ActionManifestManagerL0.cs +++ b/src/Test/L0/Worker/ActionManifestManagerL0.cs @@ -5,6 +5,7 @@ using Moq; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Runtime.CompilerServices; using System.Threading; @@ -95,6 +96,7 @@ public void Load_ContainerAction_Dockerfile_Post() Assert.Equal(containerAction.Image, "Dockerfile"); Assert.Equal(containerAction.EntryPoint, "main.sh"); Assert.Equal(containerAction.Cleanup, "cleanup.sh"); + Assert.Equal(containerAction.CleanupCondition, "failure()"); Assert.Equal(containerAction.Arguments[0].ToString(), "bzz"); Assert.Equal(containerAction.Environment[0].Key.ToString(), "Token"); Assert.Equal(containerAction.Environment[0].Value.ToString(), "foo"); @@ -309,6 +311,7 @@ public void Load_NodeAction_Cleanup() Assert.Equal(nodeAction.Script, "main.js"); Assert.Equal(nodeAction.Cleanup, "cleanup.js"); + Assert.Equal(nodeAction.CleanupCondition, "cancelled()"); } finally { @@ -427,6 +430,49 @@ public void Evaluate_ContainerAction_Env() } } + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Evaluate_Default_Input() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManager(); + actionManifest.Initialize(_hc); + + var githubContext = new DictionaryContextData(); + githubContext.Add("ref", new StringContextData("refs/heads/master")); + + var evaluateContext = new Dictionary(StringComparer.OrdinalIgnoreCase); + evaluateContext["github"] = githubContext; + evaluateContext["strategy"] = new DictionaryContextData(); + evaluateContext["matrix"] = new DictionaryContextData(); + evaluateContext["steps"] = new DictionaryContextData(); + evaluateContext["job"] = new DictionaryContextData(); + evaluateContext["runner"] = new DictionaryContextData(); + evaluateContext["env"] = new DictionaryContextData(); + + //Act + var result = actionManifest.EvaluateDefaultInput(_ec.Object, "testInput", new StringToken(null, null, null, "defaultValue"), evaluateContext); + + //Assert + Assert.Equal(result, "defaultValue"); + + //Act + result = actionManifest.EvaluateDefaultInput(_ec.Object, "testInput", new BasicExpressionToken(null, null, null, "github.ref"), evaluateContext); + + //Assert + Assert.Equal(result, "refs/heads/master"); + } + finally + { + Teardown(); + } + } + private void Setup([CallerMemberName] string name = "") { _ecTokenSource?.Dispose(); @@ -436,9 +482,10 @@ private void Setup([CallerMemberName] string name = "") _hc = new TestHostContext(this, name); _ec = new Mock(); + _ec.Setup(x => x.WriteDebug).Returns(true); _ec.Setup(x => x.CancellationToken).Returns(_ecTokenSource.Token); _ec.Setup(x => x.Variables).Returns(new Variables(_hc, new Dictionary())); - _ec.Setup(x => x.Write(It.IsAny(), It.IsAny())).Callback((string tag, string message) => { _hc.GetTrace().Info($"[{tag}]{message}"); }); + _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, string message) => { _hc.GetTrace().Info($"[{issue.Type}]{issue.Message ?? message}"); }); } diff --git a/src/Test/L0/Worker/ActionRunnerL0.cs b/src/Test/L0/Worker/ActionRunnerL0.cs index 393964fed64..141a5cd461f 100644 --- a/src/Test/L0/Worker/ActionRunnerL0.cs +++ b/src/Test/L0/Worker/ActionRunnerL0.cs @@ -30,6 +30,7 @@ public sealed class ActionRunnerL0 private Mock _ec; private TestHostContext _hc; private ActionRunner _actionRunner; + private IActionManifestManager _actionManifestManager; private string _workFolder; private DictionaryContextData _context = new DictionaryContextData(); @@ -325,6 +326,8 @@ private void Setup([CallerMemberName] string name = "") _handlerFactory = new Mock(); _defaultStepHost = new Mock(); + _actionManifestManager = new ActionManifestManager(); + _actionManifestManager.Initialize(_hc); var githubContext = new GitHubContext(); githubContext.Add("event", JToken.Parse("{\"foo\":\"bar\"}").ToPipelineContextData()); @@ -343,6 +346,7 @@ private void Setup([CallerMemberName] string name = "") _hc.SetSingleton(_actionManager.Object); _hc.SetSingleton(_handlerFactory.Object); + _hc.SetSingleton(_actionManifestManager); _hc.EnqueueInstance(_defaultStepHost.Object); diff --git a/src/Test/L0/Worker/ExecutionContextL0.cs b/src/Test/L0/Worker/ExecutionContextL0.cs index bfe8ed826b5..d39d6f2954a 100644 --- a/src/Test/L0/Worker/ExecutionContextL0.cs +++ b/src/Test/L0/Worker/ExecutionContextL0.cs @@ -206,8 +206,8 @@ public void RegisterPostJobAction_ShareState() var action2 = jobContext.CreateChild(Guid.NewGuid(), "action_2", "action_2", null, null); action2.IntraActionState["state"] = "2"; - action1.RegisterPostJobAction("post1", new Pipelines.ActionStep() { Name = "post1", DisplayName = "Test 1", Reference = new Pipelines.RepositoryPathReference() { Name = "actions/action" } }); - action2.RegisterPostJobAction("post2", new Pipelines.ActionStep() { Name = "post2", DisplayName = "Test 2", Reference = new Pipelines.RepositoryPathReference() { Name = "actions/action" } }); + action1.RegisterPostJobAction("post1", "always()", new Pipelines.ActionStep() { Name = "post1", DisplayName = "Test 1", Reference = new Pipelines.RepositoryPathReference() { Name = "actions/action" } }); + action2.RegisterPostJobAction("post2", "always()", new Pipelines.ActionStep() { Name = "post2", DisplayName = "Test 2", Reference = new Pipelines.RepositoryPathReference() { Name = "actions/action" } }); Assert.NotNull(jobContext.JobSteps); Assert.NotNull(jobContext.PostJobSteps); diff --git a/src/Test/TestData/dockerfileaction_cleanup.yml b/src/Test/TestData/dockerfileaction_cleanup.yml index ca4892ab5dd..e514d25d381 100644 --- a/src/Test/TestData/dockerfileaction_cleanup.yml +++ b/src/Test/TestData/dockerfileaction_cleanup.yml @@ -23,4 +23,5 @@ runs: env: Token: foo Url: bar - post-entrypoint: 'cleanup.sh' \ No newline at end of file + post-entrypoint: 'cleanup.sh' + post-if: 'failure()' \ No newline at end of file diff --git a/src/Test/TestData/nodeaction_cleanup.yml b/src/Test/TestData/nodeaction_cleanup.yml index 1287ba0482f..d6d522493fa 100644 --- a/src/Test/TestData/nodeaction_cleanup.yml +++ b/src/Test/TestData/nodeaction_cleanup.yml @@ -18,4 +18,5 @@ color: 'green' # optional, decorates the entry in the GitHub Marketplace runs: using: 'node12' main: 'main.js' - post: 'cleanup.js' \ No newline at end of file + post: 'cleanup.js' + post-if: 'cancelled()' \ No newline at end of file