diff --git a/src/Runner.Worker/ExecutionContext.cs b/src/Runner.Worker/ExecutionContext.cs index 667e93e8c..6464e0807 100644 --- a/src/Runner.Worker/ExecutionContext.cs +++ b/src/Runner.Worker/ExecutionContext.cs @@ -48,6 +48,7 @@ namespace GitHub.Runner.Worker PlanFeatures Features { get; } Variables Variables { get; } Dictionary IntraActionState { get; } + IDictionary> JobDefaults { get; } Dictionary JobOutputs { get; } IDictionary EnvironmentVariables { get; } IDictionary Scopes { get; } @@ -140,6 +141,7 @@ namespace GitHub.Runner.Worker public List Endpoints { get; private set; } public Variables Variables { get; private set; } public Dictionary IntraActionState { get; private set; } + public IDictionary> JobDefaults { get; private set; } public Dictionary JobOutputs { get; private set; } public IDictionary EnvironmentVariables { get; private set; } public IDictionary Scopes { get; private set; } @@ -270,6 +272,7 @@ namespace GitHub.Runner.Worker child.IntraActionState = intraActionState; } child.EnvironmentVariables = EnvironmentVariables; + child.JobDefaults = JobDefaults; child.Scopes = Scopes; child.FileTable = FileTable; child.StepsContext = StepsContext; @@ -565,6 +568,9 @@ namespace GitHub.Runner.Worker // Environment variables shared across all actions EnvironmentVariables = new Dictionary(VarUtil.EnvironmentVariableKeyComparer); + // Job defaults shared across all actions + JobDefaults = new Dictionary>(StringComparer.OrdinalIgnoreCase); + // Job Outputs JobOutputs = new Dictionary(StringComparer.OrdinalIgnoreCase); diff --git a/src/Runner.Worker/Handlers/ScriptHandler.cs b/src/Runner.Worker/Handlers/ScriptHandler.cs index ccae13501..89ac15a03 100644 --- a/src/Runner.Worker/Handlers/ScriptHandler.cs +++ b/src/Runner.Worker/Handlers/ScriptHandler.cs @@ -58,12 +58,21 @@ namespace GitHub.Runner.Worker.Handlers string shellCommandPath = null; bool validateShellOnHost = !(StepHost is ContainerStepHost); string prependPath = string.Join(Path.PathSeparator.ToString(), ExecutionContext.PrependPath.Reverse()); - Inputs.TryGetValue("shell", out var shell); + string shell = null; + if (!Inputs.TryGetValue("shell", out shell) || string.IsNullOrEmpty(shell)) + { + // TODO: figure out how defaults interact with template later + // for now, we won't check job.defaults if we are inside a template. + if (string.IsNullOrEmpty(ExecutionContext.ScopeName) && ExecutionContext.JobDefaults.TryGetValue("run", out var runDefaults)) + { + runDefaults.TryGetValue("shell", out shell); + } + } if (string.IsNullOrEmpty(shell)) { #if OS_WINDOWS shellCommand = "pwsh"; - if(validateShellOnHost) + if (validateShellOnHost) { shellCommandPath = WhichUtil.Which(shellCommand, require: false, Trace, prependPath); if (string.IsNullOrEmpty(shellCommandPath)) @@ -139,11 +148,36 @@ namespace GitHub.Runner.Worker.Handlers Inputs.TryGetValue("script", out var contents); contents = contents ?? string.Empty; - Inputs.TryGetValue("workingDirectory", out var workingDirectory); + string workingDirectory = null; + if (!Inputs.TryGetValue("workingDirectory", out workingDirectory)) + { + // TODO: figure out how defaults interact with template later + // for now, we won't check job.defaults if we are inside a template. + if (string.IsNullOrEmpty(ExecutionContext.ScopeName) && ExecutionContext.JobDefaults.TryGetValue("run", out var runDefaults)) + { + if (runDefaults.TryGetValue("working-directory", out workingDirectory)) + { + ExecutionContext.Debug("Overwrite 'working-directory' base on job defaults."); + } + } + } var workspaceDir = githubContext["workspace"] as StringContextData; workingDirectory = Path.Combine(workspaceDir, workingDirectory ?? string.Empty); - Inputs.TryGetValue("shell", out var shell); + string shell = null; + if (!Inputs.TryGetValue("shell", out shell) || string.IsNullOrEmpty(shell)) + { + // TODO: figure out how defaults interact with template later + // for now, we won't check job.defaults if we are inside a template. + if (string.IsNullOrEmpty(ExecutionContext.ScopeName) && ExecutionContext.JobDefaults.TryGetValue("run", out var runDefaults)) + { + if (runDefaults.TryGetValue("shell", out shell)) + { + ExecutionContext.Debug("Overwrite 'shell' base on job defaults."); + } + } + } + var isContainerStepHost = StepHost is ContainerStepHost; string prependPath = string.Join(Path.PathSeparator.ToString(), ExecutionContext.PrependPath.Reverse()); diff --git a/src/Runner.Worker/JobExtension.cs b/src/Runner.Worker/JobExtension.cs index c0de945a0..343b5d87e 100644 --- a/src/Runner.Worker/JobExtension.cs +++ b/src/Runner.Worker/JobExtension.cs @@ -161,6 +161,26 @@ namespace GitHub.Runner.Worker } } + // Evaluate the job defaults + context.Debug("Evaluating job defaults"); + foreach (var token in message.Defaults) + { + var defaults = token.AssertMapping("defaults"); + if (defaults.Any(x => string.Equals(x.Key.AssertString("defaults key").Value, "run", StringComparison.OrdinalIgnoreCase))) + { + context.JobDefaults["run"] = new Dictionary(StringComparer.OrdinalIgnoreCase); + var defaultsRun = defaults.First(x => string.Equals(x.Key.AssertString("defaults key").Value, "run", StringComparison.OrdinalIgnoreCase)); + var jobDefaults = templateEvaluator.EvaluateJobDefaultsRun(defaultsRun.Value, jobContext.ExpressionValues); + foreach (var pair in jobDefaults) + { + if (!string.IsNullOrEmpty(pair.Value)) + { + context.JobDefaults["run"][pair.Key] = pair.Value; + } + } + } + } + // Build up 2 lists of steps, pre-job, job // Download actions not already in the cache Trace.Info("Downloading actions"); diff --git a/src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs b/src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs index 576c3cb6f..c94ff5913 100644 --- a/src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs +++ b/src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs @@ -41,7 +41,8 @@ namespace GitHub.DistributedTask.Pipelines IEnumerable steps, IEnumerable scopes, IList fileTable, - TemplateToken jobOutputs) + TemplateToken jobOutputs, + IList defaults) { this.MessageType = JobRequestMessageTypes.PipelineAgentJobRequest; this.Plan = plan; @@ -69,6 +70,11 @@ namespace GitHub.DistributedTask.Pipelines m_environmentVariables = new List(environmentVariables); } + if (defaults?.Count > 0) + { + m_defaults = new List(defaults); + } + this.ContextData = new Dictionary(StringComparer.OrdinalIgnoreCase); if (contextData?.Count > 0) { @@ -213,6 +219,21 @@ namespace GitHub.DistributedTask.Pipelines } } + /// + /// Gets the hierarchy of defaults to overlay, last wins. + /// + public IList Defaults + { + get + { + if (m_defaults == null) + { + m_defaults = new List(); + } + return m_defaults; + } + } + /// /// Gets the collection of variables associated with the current context. /// @@ -252,6 +273,9 @@ namespace GitHub.DistributedTask.Pipelines } } + /// + /// Gets the table of files used when parsing the pipeline (e.g. yaml files) + /// public IList FileTable { get @@ -372,6 +396,11 @@ namespace GitHub.DistributedTask.Pipelines m_environmentVariables = null; } + if (m_defaults?.Count == 0) + { + m_defaults = null; + } + if (m_fileTable?.Count == 0) { m_fileTable = null; @@ -406,6 +435,9 @@ namespace GitHub.DistributedTask.Pipelines [DataMember(Name = "EnvironmentVariables", EmitDefaultValue = false)] private List m_environmentVariables; + [DataMember(Name = "Defaults", EmitDefaultValue = false)] + private List m_defaults; + [DataMember(Name = "FileTable", EmitDefaultValue = false)] private List m_fileTable; diff --git a/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConstants.cs b/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConstants.cs index 72853f5b2..86db41170 100644 --- a/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConstants.cs +++ b/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConstants.cs @@ -14,6 +14,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating public const String Clean = "clean"; public const String Container = "container"; public const String ContinueOnError = "continue-on-error"; + public const String Defaults = "defaults"; public const String Env = "env"; public const String Event = "event"; public const String EventPattern = "github.event"; @@ -29,6 +30,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating public const String Include = "include"; public const String Inputs = "inputs"; public const String Job = "job"; + public const String JobDefaultsRun = "job-defaults-run"; public const String JobOutputs = "job-outputs"; public const String Jobs = "jobs"; public const String Labels = "labels"; diff --git a/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateEvaluator.cs b/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateEvaluator.cs index ea9193ee1..d60fcc529 100644 --- a/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateEvaluator.cs +++ b/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateEvaluator.cs @@ -267,6 +267,42 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating return result; } + public Dictionary EvaluateJobDefaultsRun( + TemplateToken token, + DictionaryContextData contextData) + { + var result = default(Dictionary); + + if (token != null && token.Type != TokenType.Null) + { + var context = CreateContext(contextData); + try + { + token = TemplateEvaluator.Evaluate(context, PipelineTemplateConstants.JobDefaultsRun, token, 0, null, omitHeader: true); + context.Errors.Check(); + result = new Dictionary(StringComparer.OrdinalIgnoreCase); + var mapping = token.AssertMapping("defaults run"); + foreach (var pair in mapping) + { + // Literal key + var key = pair.Key.AssertString("defaults run key"); + + // Literal value + var value = pair.Value.AssertString("defaults run value"); + result[key.Value] = value.Value; + } + } + catch (Exception ex) when (!(ex is TemplateValidationException)) + { + context.Errors.Add(ex); + } + + context.Errors.Check(); + } + + return result; + } + public IList> EvaluateJobServiceContainers( TemplateToken token, DictionaryContextData contextData) diff --git a/src/Sdk/DTPipelines/workflow-v1.0.json b/src/Sdk/DTPipelines/workflow-v1.0.json index 203a686ad..5c8c4c43e 100644 --- a/src/Sdk/DTPipelines/workflow-v1.0.json +++ b/src/Sdk/DTPipelines/workflow-v1.0.json @@ -9,6 +9,7 @@ "properties": { "on": "any", "name": "string", + "defaults": "workflow-defaults", "env": "workflow-env", "jobs": "jobs" } @@ -125,6 +126,23 @@ "string": {} }, + "workflow-defaults": { + "mapping": { + "properties": { + "run": "workflow-defaults-run" + } + } + }, + + "workflow-defaults-run": { + "mapping": { + "properties": { + "shell": "non-empty-string", + "working-directory": "non-empty-string" + } + } + }, + "workflow-env": { "context": [ "github", @@ -161,6 +179,7 @@ "services": "services", "env": "job-env", "outputs": "job-outputs", + "defaults": "job-defaults", "steps": "steps" } } @@ -289,6 +308,30 @@ } }, + "job-defaults": { + "mapping": { + "properties": { + "run": "job-defaults-run" + } + } + }, + + "job-defaults-run": { + "context": [ + "github", + "strategy", + "matrix", + "needs", + "env" + ], + "mapping": { + "properties": { + "shell": "non-empty-string", + "working-directory": "non-empty-string" + } + } + }, + "job-outputs": { "mapping": { "loose-key-type": "non-empty-string", diff --git a/src/Test/L0/Listener/JobDispatcherL0.cs b/src/Test/L0/Listener/JobDispatcherL0.cs index 6ee79360e..00a7b5155 100644 --- a/src/Test/L0/Listener/JobDispatcherL0.cs +++ b/src/Test/L0/Listener/JobDispatcherL0.cs @@ -33,7 +33,7 @@ namespace GitHub.Runner.Common.Tests.Listener TaskOrchestrationPlanReference plan = new TaskOrchestrationPlanReference(); TimelineReference timeline = null; Guid jobId = Guid.NewGuid(); - var result = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, "someJob", "someJob", null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null); + var result = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, "someJob", "someJob", null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); result.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData(); return result; } diff --git a/src/Test/L0/Listener/RunnerL0.cs b/src/Test/L0/Listener/RunnerL0.cs index 5d8bc3e0f..32a21521b 100644 --- a/src/Test/L0/Listener/RunnerL0.cs +++ b/src/Test/L0/Listener/RunnerL0.cs @@ -43,7 +43,7 @@ namespace GitHub.Runner.Common.Tests.Listener TaskOrchestrationPlanReference plan = new TaskOrchestrationPlanReference(); TimelineReference timeline = null; Guid jobId = Guid.NewGuid(); - return new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, "test", "test", null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null); + return new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, "test", "test", null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); } private JobCancelMessage CreateJobCancelMessage() diff --git a/src/Test/L0/Worker/ActionCommandManagerL0.cs b/src/Test/L0/Worker/ActionCommandManagerL0.cs index 2ef9f5c5c..568c1a86a 100644 --- a/src/Test/L0/Worker/ActionCommandManagerL0.cs +++ b/src/Test/L0/Worker/ActionCommandManagerL0.cs @@ -150,7 +150,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new TimelineReference(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, diff --git a/src/Test/L0/Worker/ExecutionContextL0.cs b/src/Test/L0/Worker/ExecutionContextL0.cs index 190bcda76..aef52d402 100644 --- a/src/Test/L0/Worker/ExecutionContextL0.cs +++ b/src/Test/L0/Worker/ExecutionContextL0.cs @@ -25,7 +25,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new TimelineReference(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, @@ -101,7 +101,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new TimelineReference(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, @@ -152,7 +152,7 @@ namespace GitHub.Runner.Common.Tests.Worker TimelineReference timeline = new TimelineReference(); Guid jobId = Guid.NewGuid(); string jobName = "some job name"; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, jobName, jobName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() { Alias = Pipelines.PipelineConstants.SelfAlias, diff --git a/src/Test/L0/Worker/JobExtensionL0.cs b/src/Test/L0/Worker/JobExtensionL0.cs index b2c5dbddd..bedd24c36 100644 --- a/src/Test/L0/Worker/JobExtensionL0.cs +++ b/src/Test/L0/Worker/JobExtensionL0.cs @@ -100,7 +100,7 @@ namespace GitHub.Runner.Common.Tests.Worker }; Guid jobId = Guid.NewGuid(); - _message = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, "test", "test", null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), steps, null, null, null); + _message = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, "test", "test", null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), steps, null, null, null, null); GitHubContext github = new GitHubContext(); github["repository"] = new Pipelines.ContextData.StringContextData("actions/runner"); _message.ContextData.Add("github", github); diff --git a/src/Test/L0/Worker/JobRunnerL0.cs b/src/Test/L0/Worker/JobRunnerL0.cs index 574654720..de09a4c96 100644 --- a/src/Test/L0/Worker/JobRunnerL0.cs +++ b/src/Test/L0/Worker/JobRunnerL0.cs @@ -63,7 +63,7 @@ namespace GitHub.Runner.Common.Tests.Worker TaskOrchestrationPlanReference plan = new TaskOrchestrationPlanReference(); TimelineReference timeline = new Timeline(Guid.NewGuid()); Guid jobId = Guid.NewGuid(); - _message = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, testName, testName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null); + _message = new Pipelines.AgentJobRequestMessage(plan, timeline, jobId, testName, testName, null, null, null, new Dictionary(), new List(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List(), null, null, null, null); _message.Variables[Constants.Variables.System.Culture] = "en-US"; _message.Resources.Endpoints.Add(new ServiceEndpoint() { diff --git a/src/Test/L0/Worker/WorkerL0.cs b/src/Test/L0/Worker/WorkerL0.cs index ed930f184..80e5eaa0a 100644 --- a/src/Test/L0/Worker/WorkerL0.cs +++ b/src/Test/L0/Worker/WorkerL0.cs @@ -67,7 +67,7 @@ namespace GitHub.Runner.Common.Tests.Worker new Pipelines.ContextData.DictionaryContextData() }, }; - var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, JobId, jobName, jobName, new StringToken(null, null, null, "ubuntu"), sidecarContainers, null, variables, new List(), resources, context, null, actions, null, null, null); + var jobRequest = new Pipelines.AgentJobRequestMessage(plan, timeline, JobId, jobName, jobName, new StringToken(null, null, null, "ubuntu"), sidecarContainers, null, variables, new List(), resources, context, null, actions, null, null, null, null); return jobRequest; }