mirror of
https://github.com/actions/runner.git
synced 2026-07-03 11:06:08 +08:00
Hook job lifecycle into DAP debugger behind containment gate
Wires three runner-internal call sites into the DAP debugger:
- JobRunner: notifies the debugger once Pre/Main/Post step lists
are populated, so it can seed the execution view and predict
Post placeholders.
- ExecutionContext.RegisterPostJobStep: notifies the debugger when
a post step is registered, so it can claim the predicted
placeholder or append a new entry.
- StepsRunner: notifies the debugger when a Main step is skipped
by its if: condition, so predicted Post placeholders for that
step can be marked as skipped.
Each call site gates on jobContext.Global.Debugger?.Enabled == true
before calling HostContext.GetService<IDapDebugger>(). Without the
gate, GetService<T>() auto-instantiates the default singleton for
every non-debug job, breaking the 'no debugger, no risk' containment
property.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -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<IDapDebugger>() 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<Dap.IDapDebugger>().OnPostStepRegistered(step);
|
||||
}
|
||||
}
|
||||
|
||||
public IExecutionContext CreateChild(
|
||||
|
||||
@@ -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<IDapDebugger>() would auto-
|
||||
// instantiate the default singleton for every non-debug job, violating the
|
||||
// "no debugger, no risk" containment property.
|
||||
var dapDebugger = HostContext.GetService<Dap.IDapDebugger>();
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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<IDapDebugger>().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<IDapDebugger>().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<string, VariableValue>(), new List<MaskHint>(), new Pipelines.JobResources(), new Pipelines.ContextData.DictionaryContextData(), new Pipelines.WorkspaceOptions(), new List<Pipelines.ActionStep>(), 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<IPagingLogger>();
|
||||
var jobServerQueue = new Mock<IJobServerQueue>();
|
||||
jobServerQueue.Setup(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.IsAny<TimelineRecord>()));
|
||||
jobServerQueue.Setup(x => x.QueueWebConsoleLine(It.IsAny<Guid>(), It.IsAny<string>(), It.IsAny<long?>()));
|
||||
|
||||
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<IDapDebugger>().
|
||||
var dapMock = new Mock<IDapDebugger>(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<IActionRunner>();
|
||||
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")]
|
||||
|
||||
@@ -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<IDapDebugger>().Object);
|
||||
hc.EnqueueInstance<IPagingLogger>(_logger.Object); // JobExecutionContext
|
||||
hc.EnqueueInstance<IPagingLogger>(_logger.Object); // job start hook
|
||||
hc.EnqueueInstance<IPagingLogger>(_logger.Object); // Initial Job
|
||||
|
||||
@@ -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<IDapDebugger>().Object);
|
||||
hc.EnqueueInstance<IExecutionContext>(_jobEc);
|
||||
hc.EnqueueInstance<IPagingLogger>(_logger.Object);
|
||||
hc.EnqueueInstance<IJobExtension>(_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<IDapDebugger>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user