diff --git a/src/Runner.Worker/ExecutionContext.cs b/src/Runner.Worker/ExecutionContext.cs index f072335b4..03fdf4414 100644 --- a/src/Runner.Worker/ExecutionContext.cs +++ b/src/Runner.Worker/ExecutionContext.cs @@ -338,6 +338,14 @@ namespace GitHub.Runner.Worker step.ExecutionContext = Root.CreatePostChild(step.DisplayName, IntraActionState, siblingScopeName); Root.PostJobSteps.Push(step); + // Only consult the DAP debugger when it was actually enabled for this job. + // Without this guard, HostContext.GetService() would auto- + // instantiate the default singleton for every non-debug job, violating the + // "no debugger, no risk" containment property. + if (Global.Debugger?.Enabled == true) + { + HostContext.GetService().OnPostStepRegistered(step); + } } public IExecutionContext CreateChild( diff --git a/src/Runner.Worker/JobRunner.cs b/src/Runner.Worker/JobRunner.cs index 8308b4342..3efaa9576 100644 --- a/src/Runner.Worker/JobRunner.cs +++ b/src/Runner.Worker/JobRunner.cs @@ -230,6 +230,24 @@ namespace GitHub.Runner.Worker jobContext.JobSteps.Enqueue(step); } + if (jobContext.Global.Debugger?.Enabled == true) + { + // Only consult the DAP debugger when it was actually enabled for this job. + // Without this guard, HostContext.GetService() would auto- + // instantiate the default singleton for every non-debug job, violating the + // "no debugger, no risk" containment property. + var dapDebugger = HostContext.GetService(); + try + { + await dapDebugger.OnJobStepsInitializedAsync(jobContext.JobSteps, jobContext.PostJobSteps); + } + catch (Exception ex) + { + Trace.Warning("DAP OnJobStepsInitialized error; continuing without DAP view."); + Trace.Error(ex); + } + } + await stepsRunner.RunAsync(jobContext); } catch (Exception ex) diff --git a/src/Runner.Worker/StepsRunner.cs b/src/Runner.Worker/StepsRunner.cs index 21bdfa6f7..20bea93a5 100644 --- a/src/Runner.Worker/StepsRunner.cs +++ b/src/Runner.Worker/StepsRunner.cs @@ -219,12 +219,18 @@ namespace GitHub.Runner.Worker // Condition is false Trace.Info("Skipping step due to condition evaluation."); CompleteStep(step, TaskResult.Skipped, resultCode: conditionTraceWriter.Trace); + // Notify the DAP debugger so any predicted Post-step + // placeholder for this Main step can be marked as + // skipped — otherwise the rendered view leaves a + // stale "Post X" entry for a step that never ran. + dapDebugger?.OnStepCompleted(step); } else if (conditionEvaluateError != null) { // Condition error step.ExecutionContext.Error(conditionEvaluateError); CompleteStep(step, TaskResult.Failed); + dapDebugger?.OnStepCompleted(step); } else { diff --git a/src/Test/L0/Worker/ExecutionContextL0.cs b/src/Test/L0/Worker/ExecutionContextL0.cs index d35be3acc..dadd1db91 100644 --- a/src/Test/L0/Worker/ExecutionContextL0.cs +++ b/src/Test/L0/Worker/ExecutionContextL0.cs @@ -7,6 +7,7 @@ using GitHub.DistributedTask.Pipelines.ContextData; using GitHub.DistributedTask.WebApi; using GitHub.Runner.Worker; using GitHub.Runner.Worker.Container; +using GitHub.Runner.Worker.Dap; using GitHub.Runner.Worker.Handlers; using Moq; using Xunit; @@ -405,6 +406,7 @@ namespace GitHub.Runner.Common.Tests.Worker hc.EnqueueInstance(pagingLogger5.Object); hc.EnqueueInstance(actionRunner1 as IActionRunner); hc.EnqueueInstance(actionRunner2 as IActionRunner); + hc.SetSingleton(new Mock().Object); hc.SetSingleton(jobServerQueue.Object); var jobContext = new Runner.Worker.ExecutionContext(); @@ -503,6 +505,7 @@ namespace GitHub.Runner.Common.Tests.Worker hc.EnqueueInstance(pagingLogger5.Object); hc.EnqueueInstance(actionRunner1 as IActionRunner); hc.EnqueueInstance(actionRunner2 as IActionRunner); + hc.SetSingleton(new Mock().Object); hc.SetSingleton(jobServerQueue.Object); var jobContext = new Runner.Worker.ExecutionContext(); @@ -544,6 +547,75 @@ namespace GitHub.Runner.Common.Tests.Worker } } + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void RegisterPostJobAction_DebuggerDisabled_DoesNotInvokeDapDebugger() + { + using (TestHostContext hc = CreateTestContext()) + { + // Arrange: Create a job request message with EnableDebugger left at the default (false). + TaskOrchestrationPlanReference plan = new(); + TimelineReference timeline = new(); + 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, null); + jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource() + { + Alias = Pipelines.PipelineConstants.SelfAlias, + Id = "github", + Version = "sha1" + }); + jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData(); + + 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())); + + var actionRunner = new ActionRunner(); + actionRunner.Initialize(hc); + + hc.EnqueueInstance(pagingLogger.Object); + hc.EnqueueInstance(pagingLogger.Object); + hc.EnqueueInstance(pagingLogger.Object); + hc.EnqueueInstance(pagingLogger.Object); + hc.EnqueueInstance(pagingLogger.Object); + hc.EnqueueInstance(pagingLogger.Object); + hc.EnqueueInstance(pagingLogger.Object); + hc.EnqueueInstance(actionRunner as IActionRunner); + + // Register a strict mock IDapDebugger. If the production code calls + // ANY method on it, the test fails — proving the containment guard + // short-circuited before HostContext.GetService(). + var dapMock = new Mock(MockBehavior.Strict); + hc.SetSingleton(dapMock.Object); + hc.SetSingleton(jobServerQueue.Object); + + var jobContext = new Runner.Worker.ExecutionContext(); + jobContext.Initialize(hc); + jobContext.InitializeJob(jobRequest, CancellationToken.None); + + var action = jobContext.CreateChild(Guid.NewGuid(), "action_1", "action_1", null, null, 0); + + var postRunner = hc.CreateService(); + postRunner.Action = new Pipelines.ActionStep() { Id = Guid.NewGuid(), Name = "post", DisplayName = "Post", Reference = new Pipelines.RepositoryPathReference() { Name = "actions/action" } }; + postRunner.Stage = ActionRunStage.Post; + postRunner.Condition = "always()"; + postRunner.DisplayName = "post"; + + // Sanity: ensure the production code path actually believes the debugger is disabled. + Assert.True(jobContext.Global.Debugger == null || jobContext.Global.Debugger.Enabled == false); + + // Act. + action.RegisterPostJobStep(postRunner); + + // Assert: the debugger was never consulted on the non-debug path. + dapMock.VerifyNoOtherCalls(); + Assert.Equal(1, jobContext.PostJobSteps.Count); + } + } + [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] diff --git a/src/Test/L0/Worker/JobExtensionL0.cs b/src/Test/L0/Worker/JobExtensionL0.cs index a286e62db..2a741de37 100644 --- a/src/Test/L0/Worker/JobExtensionL0.cs +++ b/src/Test/L0/Worker/JobExtensionL0.cs @@ -141,6 +141,7 @@ namespace GitHub.Runner.Common.Tests.Worker hc.SetSingleton(_diagnosticLogManager.Object); hc.SetSingleton(_jobHookProvider.Object); hc.SetSingleton(_snapshotOperationProvider.Object); + hc.SetSingleton(new Mock().Object); hc.EnqueueInstance(_logger.Object); // JobExecutionContext hc.EnqueueInstance(_logger.Object); // job start hook hc.EnqueueInstance(_logger.Object); // Initial Job diff --git a/src/Test/L0/Worker/JobRunnerL0.cs b/src/Test/L0/Worker/JobRunnerL0.cs index e8011b9b0..e47c478e3 100644 --- a/src/Test/L0/Worker/JobRunnerL0.cs +++ b/src/Test/L0/Worker/JobRunnerL0.cs @@ -1,5 +1,6 @@ using GitHub.DistributedTask.WebApi; using GitHub.Runner.Worker; +using GitHub.Runner.Worker.Dap; using Moq; using System; using System.Collections.Generic; @@ -83,6 +84,7 @@ namespace GitHub.Runner.Common.Tests.Worker hc.SetSingleton(_extensions.Object); hc.SetSingleton(_temp.Object); hc.SetSingleton(_diagnosticLogManager.Object); + hc.SetSingleton(new Mock().Object); hc.EnqueueInstance(_jobEc); hc.EnqueueInstance(_logger.Object); hc.EnqueueInstance(_jobExtension.Object); @@ -175,5 +177,29 @@ namespace GitHub.Runner.Common.Tests.Worker Assert.Equal(TaskResult.Succeeded, _jobEc.Result); } } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task DebuggerDisabled_DoesNotInvokeDapDebugger() + { + using (TestHostContext hc = CreateTestContext()) + { + // Override the lenient IDapDebugger singleton from CreateTestContext + // with a strict mock. If the containment guard fails, the production + // code will call OnJobStepsInitializedAsync and the strict mock will throw. + var dapMock = new Mock(MockBehavior.Strict); + hc.SetSingleton(dapMock.Object); + + var message = GetMessage(); + // EnableDebugger defaults to false on AgentJobRequestMessage. + Assert.False(message.EnableDebugger); + + await _jobRunner.RunAsync(message, _tokenSource.Token); + + Assert.Equal(TaskResult.Succeeded, _jobEc.Result); + dapMock.VerifyNoOtherCalls(); + } + } } }