diff --git a/src/Runner.Worker/ExecutionContext.cs b/src/Runner.Worker/ExecutionContext.cs index 78cf2ab66..a27033709 100644 --- a/src/Runner.Worker/ExecutionContext.cs +++ b/src/Runner.Worker/ExecutionContext.cs @@ -11,6 +11,7 @@ using System.Threading; using System.Threading.Tasks; using System.Web; using GitHub.DistributedTask.Expressions2; +using GitHub.DistributedTask.ObjectTemplating.Tokens; using GitHub.DistributedTask.Pipelines; using GitHub.DistributedTask.Pipelines.ContextData; using GitHub.DistributedTask.Pipelines.ObjectTemplating; @@ -109,7 +110,12 @@ namespace GitHub.Runner.Worker void ForceTaskComplete(); void RegisterPostJobStep(IStep step); void PublishStepTelemetry(); + + void ApplyContinueOnError(TemplateToken continueOnError); + void UpdateGlobalStepsContext(); + void WriteWebhookPayload(); + } public sealed class ExecutionContext : RunnerService, IExecutionContext @@ -439,14 +445,19 @@ namespace GitHub.Runner.Worker _logger.End(); + UpdateGlobalStepsContext(); + + return Result.Value; + } + + public void UpdateGlobalStepsContext() + { // Skip if generated context name. Generated context names start with "__". After 3.2 the server will never send an empty context name. if (!string.IsNullOrEmpty(ContextName) && !ContextName.StartsWith("__", StringComparison.Ordinal)) { Global.StepsContext.SetOutcome(ScopeName, ContextName, (Outcome ?? Result ?? TaskResult.Succeeded).ToActionResult()); Global.StepsContext.SetConclusion(ScopeName, ContextName, (Result ?? TaskResult.Succeeded).ToActionResult()); } - - return Result.Value; } public void SetRunnerContext(string name, string value) @@ -1064,6 +1075,36 @@ namespace GitHub.Runner.Worker var newGuid = Guid.NewGuid(); return CreateChild(newGuid, displayName, newGuid.ToString("N"), null, null, ActionRunStage.Post, intraActionState, _childTimelineRecordOrder - Root.PostJobSteps.Count, siblingScopeName: siblingScopeName); } + + public void ApplyContinueOnError(TemplateToken continueOnErrorToken) + { + if (Result != TaskResult.Failed) + { + return; + } + var continueOnError = false; + try + { + var templateEvaluator = this.ToPipelineTemplateEvaluator(); + continueOnError = templateEvaluator.EvaluateStepContinueOnError(continueOnErrorToken, ExpressionValues, ExpressionFunctions); + } + catch (Exception ex) + { + Trace.Info("The step failed and an error occurred when attempting to determine whether to continue on error."); + Trace.Error(ex); + this.Error("The step failed and an error occurred when attempting to determine whether to continue on error."); + this.Error(ex); + } + + if (continueOnError) + { + Outcome = Result; + Result = TaskResult.Succeeded; + Trace.Info($"Updated step result (continue on error)"); + } + + UpdateGlobalStepsContext(); + } } // The Error/Warning/etc methods are created as extension methods to simplify unit testing. @@ -1085,7 +1126,6 @@ namespace GitHub.Runner.Worker context.Error(ex.Message); context.Debug(ex.ToString()); } - // Do not add a format string overload. See comment on ExecutionContext.Write(). public static void Error(this IExecutionContext context, string message) { diff --git a/src/Runner.Worker/Handlers/CompositeActionHandler.cs b/src/Runner.Worker/Handlers/CompositeActionHandler.cs index 425d19892..8290bb874 100644 --- a/src/Runner.Worker/Handlers/CompositeActionHandler.cs +++ b/src/Runner.Worker/Handlers/CompositeActionHandler.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; -using System.Text; using System.Threading; using System.Threading.Tasks; using GitHub.DistributedTask.Expressions2; @@ -13,7 +11,6 @@ using GitHub.DistributedTask.WebApi; using GitHub.Runner.Common; using GitHub.Runner.Common.Util; using GitHub.Runner.Sdk; -using GitHub.Runner.Worker; using GitHub.Runner.Worker.Expressions; using Pipelines = GitHub.DistributedTask.Pipelines; @@ -86,7 +83,7 @@ namespace GitHub.Runner.Worker.Handlers ExecutionContext.StepTelemetry.HasPreStep = Data.HasPre; ExecutionContext.StepTelemetry.HasPostStep = Data.HasPost; - + ExecutionContext.StepTelemetry.HasRunsStep = hasRunsStep; ExecutionContext.StepTelemetry.HasUsesStep = hasUsesStep; ExecutionContext.StepTelemetry.StepCount = steps.Count; @@ -407,7 +404,7 @@ namespace GitHub.Runner.Worker.Handlers } // Update context - SetStepsContext(step); + step.ExecutionContext.UpdateGlobalStepsContext(); } } @@ -452,6 +449,8 @@ namespace GitHub.Runner.Worker.Handlers SetStepConclusion(step, Common.Util.TaskResultUtil.MergeTaskResults(step.ExecutionContext.Result, step.ExecutionContext.CommandResult.Value)); } + step.ExecutionContext.ApplyContinueOnError(step.ContinueOnError); + Trace.Info($"Step result: {step.ExecutionContext.Result}"); step.ExecutionContext.Debug($"Finished: {step.DisplayName}"); step.ExecutionContext.PublishStepTelemetry(); @@ -460,16 +459,7 @@ namespace GitHub.Runner.Worker.Handlers private void SetStepConclusion(IStep step, TaskResult result) { step.ExecutionContext.Result = result; - SetStepsContext(step); - } - private void SetStepsContext(IStep step) - { - if (!string.IsNullOrEmpty(step.ExecutionContext.ContextName) && !step.ExecutionContext.ContextName.StartsWith("__", StringComparison.Ordinal)) - { - // TODO: when we support continue on error, we may need to do logic here to change conclusion based on the continue on error result - step.ExecutionContext.Global.StepsContext.SetOutcome(step.ExecutionContext.ScopeName, step.ExecutionContext.ContextName, (step.ExecutionContext.Result ?? TaskResult.Succeeded).ToActionResult()); - step.ExecutionContext.Global.StepsContext.SetConclusion(step.ExecutionContext.ScopeName, step.ExecutionContext.ContextName, (step.ExecutionContext.Result ?? TaskResult.Succeeded).ToActionResult()); - } + step.ExecutionContext.UpdateGlobalStepsContext(); } } } diff --git a/src/Runner.Worker/StepsRunner.cs b/src/Runner.Worker/StepsRunner.cs index 47f9d4c59..e58ff039c 100644 --- a/src/Runner.Worker/StepsRunner.cs +++ b/src/Runner.Worker/StepsRunner.cs @@ -320,29 +320,8 @@ namespace GitHub.Runner.Worker step.ExecutionContext.Result = TaskResultUtil.MergeTaskResults(step.ExecutionContext.Result, step.ExecutionContext.CommandResult.Value); } - // Fixup the step result if ContinueOnError - if (step.ExecutionContext.Result == TaskResult.Failed) - { - var continueOnError = false; - try - { - continueOnError = templateEvaluator.EvaluateStepContinueOnError(step.ContinueOnError, step.ExecutionContext.ExpressionValues, step.ExecutionContext.ExpressionFunctions); - } - catch (Exception ex) - { - Trace.Info("The step failed and an error occurred when attempting to determine whether to continue on error."); - Trace.Error(ex); - step.ExecutionContext.Error("The step failed and an error occurred when attempting to determine whether to continue on error."); - step.ExecutionContext.Error(ex); - } + step.ExecutionContext.ApplyContinueOnError(step.ContinueOnError); - if (continueOnError) - { - step.ExecutionContext.Outcome = step.ExecutionContext.Result; - step.ExecutionContext.Result = TaskResult.Succeeded; - Trace.Info($"Updated step result (continue on error)"); - } - } Trace.Info($"Step result: {step.ExecutionContext.Result}"); // Complete the step context diff --git a/src/Runner.Worker/action_yaml.json b/src/Runner.Worker/action_yaml.json index b01d91348..32771feb8 100644 --- a/src/Runner.Worker/action_yaml.json +++ b/src/Runner.Worker/action_yaml.json @@ -129,6 +129,7 @@ "required": true }, "env": "step-env", + "continue-on-error": "boolean-steps-context", "working-directory": "string-steps-context", "shell": { "type": "non-empty-string", @@ -147,6 +148,7 @@ "type": "non-empty-string", "required": true }, + "continue-on-error": "boolean-steps-context", "with": "step-with", "env": "step-env" } @@ -201,6 +203,20 @@ ], "string": {} }, + "boolean-steps-context": { + "context": [ + "github", + "inputs", + "strategy", + "matrix", + "steps", + "job", + "runner", + "env", + "hashFiles(1,255)" + ], + "boolean": {} + }, "step-env": { "context": [ "github", diff --git a/src/Test/L0/Worker/ExecutionContextL0.cs b/src/Test/L0/Worker/ExecutionContextL0.cs index aa50c71fa..4ae7cc0d5 100644 --- a/src/Test/L0/Worker/ExecutionContextL0.cs +++ b/src/Test/L0/Worker/ExecutionContextL0.cs @@ -8,6 +8,7 @@ using GitHub.DistributedTask.WebApi; using GitHub.Runner.Worker; using Moq; using Xunit; +using GitHub.DistributedTask.ObjectTemplating.Tokens; using Pipelines = GitHub.DistributedTask.Pipelines; namespace GitHub.Runner.Common.Tests.Worker @@ -90,6 +91,63 @@ namespace GitHub.Runner.Common.Tests.Worker } } + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ApplyContinueOnError_CheckResultAndOutcome() + { + using (TestHostContext hc = CreateTestContext()) + { + + // Arrange: Create a job request message. + TaskOrchestrationPlanReference plan = new TaskOrchestrationPlanReference(); + 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, null); + jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() + { + Alias = Pipelines.PipelineConstants.SelfAlias, + Id = "github", + Version = "sha1" + }); + jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData(); + jobRequest.Variables["ACTIONS_STEP_DEBUG"] = "true"; + + // Arrange: Setup the paging logger. + var pagingLogger = new Mock(); + var jobServerQueue = new Mock(); + jobServerQueue.Setup(x => x.QueueTimelineRecordUpdate(It.IsAny(), It.IsAny())); + jobServerQueue.Setup(x => x.QueueWebConsoleLine(It.IsAny(), It.IsAny(), It.IsAny())).Callback((Guid id, string msg, long? lineNumber) => { hc.GetTrace().Info(msg); }); + + hc.EnqueueInstance(pagingLogger.Object); + hc.SetSingleton(jobServerQueue.Object); + + var ec = new Runner.Worker.ExecutionContext(); + ec.Initialize(hc); + + // Act. + ec.InitializeJob(jobRequest, CancellationToken.None); + + foreach (var tc in new List<(TemplateToken token, TaskResult result, TaskResult? expectedResult, TaskResult? expectedOutcome)> { + (token: new BooleanToken(null, null, null, true), result: TaskResult.Failed, expectedResult: TaskResult.Succeeded, expectedOutcome: TaskResult.Failed), + (token: new BooleanToken(null, null, null, true), result: TaskResult.Succeeded, expectedResult: TaskResult.Succeeded, expectedOutcome: null), + (token: new BooleanToken(null, null, null, true), result: TaskResult.Canceled, expectedResult: TaskResult.Canceled, expectedOutcome: null), + (token: new BooleanToken(null, null, null, false), result: TaskResult.Failed, expectedResult: TaskResult.Failed, expectedOutcome: null), + (token: new BooleanToken(null, null, null, false), result: TaskResult.Succeeded, expectedResult: TaskResult.Succeeded, expectedOutcome: null), + (token: new BooleanToken(null, null, null, false), result: TaskResult.Canceled, expectedResult: TaskResult.Canceled, expectedOutcome: null), + }) + { + ec.Result = tc.result; + ec.Outcome = null; + ec.ApplyContinueOnError(tc.token); + Assert.Equal(ec.Result, tc.expectedResult); + Assert.Equal(ec.Outcome, tc.expectedOutcome); + } + } + } + + [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] diff --git a/src/Test/L0/Worker/StepsRunnerL0.cs b/src/Test/L0/Worker/StepsRunnerL0.cs index 86843a083..6f1cad8ab 100644 --- a/src/Test/L0/Worker/StepsRunnerL0.cs +++ b/src/Test/L0/Worker/StepsRunnerL0.cs @@ -622,6 +622,40 @@ namespace GitHub.Runner.Common.Tests.Worker _stepContext.SetOutcome("", stepContext.Object.ContextName, (stepContext.Object.Outcome ?? stepContext.Object.Result ?? TaskResult.Succeeded).ToActionResult()); _stepContext.SetConclusion("", stepContext.Object.ContextName, (stepContext.Object.Result ?? TaskResult.Succeeded).ToActionResult()); }); + + stepContext.Setup(x => x.UpdateGlobalStepsContext()).Callback(() => + { + if (!string.IsNullOrEmpty(stepContext.Object.ContextName) && !stepContext.Object.ContextName.StartsWith("__", StringComparison.Ordinal)) + { + stepContext.Object.Global.StepsContext.SetOutcome(stepContext.Object.ScopeName, stepContext.Object.ContextName, (stepContext.Object.Outcome ?? stepContext.Object.Result ?? TaskResult.Succeeded).ToActionResult()); + stepContext.Object.Global.StepsContext.SetConclusion(stepContext.Object.ScopeName, stepContext.Object.ContextName, (stepContext.Object.Result ?? TaskResult.Succeeded).ToActionResult()); + } + }); + stepContext.Setup(x => x.ApplyContinueOnError(It.IsAny())).Callback((TemplateToken token) => + { + if (stepContext.Object.Result != TaskResult.Failed) + { + return; + } + var continueOnError = false; + try + { + var templateEvaluator = stepContext.Object.ToPipelineTemplateEvaluator(); + continueOnError = templateEvaluator.EvaluateStepContinueOnError(token, stepContext.Object.ExpressionValues, stepContext.Object.ExpressionFunctions); + } + catch (Exception ex) + { + stepContext.Object.Error("The step failed and an error occurred when attempting to determine whether to continue on error."); + stepContext.Object.Error(ex); + } + + if (continueOnError) + { + stepContext.Object.Outcome = stepContext.Object.Result; + stepContext.Object.Result = TaskResult.Succeeded; + } + stepContext.Object.UpdateGlobalStepsContext(); + }); var trace = hc.GetTrace(); stepContext.Setup(x => x.Write(It.IsAny(), It.IsAny())).Callback((string tag, string message) => { trace.Info($"[{tag}]{message}"); }); stepContext.Object.Result = result;