mirror of
https://github.com/actions/runner.git
synced 2026-07-03 11:06:08 +08:00
Wire job execution view into DAP (#4471)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -16,6 +16,7 @@ using Microsoft.DevTunnels.Connections;
|
||||
using Microsoft.DevTunnels.Contracts;
|
||||
using Microsoft.DevTunnels.Management;
|
||||
using Newtonsoft.Json;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
@@ -27,6 +28,7 @@ namespace GitHub.Runner.Worker.Dap
|
||||
public string DisplayName { get; set; }
|
||||
public TaskResult? Result { get; set; }
|
||||
public int FrameId { get; set; }
|
||||
public int? SourceLine { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -54,6 +56,9 @@ namespace GitHub.Runner.Worker.Dap
|
||||
// Frame IDs for completed steps start at 1000
|
||||
private const int _completedFrameIdBase = 1000;
|
||||
|
||||
// Stable session-scoped source reference for the synthesized job step list.
|
||||
private const int _jobStepsSourceReference = 1;
|
||||
|
||||
private TcpListener _listener;
|
||||
private TcpClient _client;
|
||||
private NetworkStream _stream;
|
||||
@@ -98,6 +103,8 @@ namespace GitHub.Runner.Worker.Dap
|
||||
// Track completed steps for stack trace
|
||||
private readonly List<CompletedStepInfo> _completedSteps = new List<CompletedStepInfo>();
|
||||
private int _nextCompletedFrameId = _completedFrameIdBase;
|
||||
private JobExecutionView _jobStepsSource;
|
||||
private bool _jobCompleted;
|
||||
|
||||
// Client connection tracking for reconnection support
|
||||
private volatile bool _isClientConnected;
|
||||
@@ -240,6 +247,179 @@ namespace GitHub.Runner.Worker.Dap
|
||||
}
|
||||
}
|
||||
|
||||
public Task OnJobStepsInitializedAsync(IEnumerable<IStep> steps, IEnumerable<IStep> initialPostSteps)
|
||||
{
|
||||
if (!IsActive)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IExecutionContext jobContext;
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (_state != DapSessionState.Ready &&
|
||||
_state != DapSessionState.Paused &&
|
||||
_state != DapSessionState.Running)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
jobContext = _jobContext;
|
||||
}
|
||||
|
||||
var stepList = steps?.Where(step => step != null).ToList() ?? new List<IStep>();
|
||||
var initialPostStepList = initialPostSteps?.Where(step => step != null).ToList() ?? new List<IStep>();
|
||||
var jobId = jobContext?.GetGitHubContext("job");
|
||||
var snapshot = new JobExecutionView(
|
||||
jobId,
|
||||
stepList,
|
||||
initialPostStepList,
|
||||
PredictPostSteps(jobContext, stepList, initialPostStepList));
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
_jobStepsSource = snapshot;
|
||||
_jobCompleted = false;
|
||||
}
|
||||
Trace.Info("DAP job steps source initialized");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning("DAP OnJobStepsInitialized error.");
|
||||
Trace.Error(ex);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void OnPostStepRegistered(IStep step)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (step is IActionRunner postRunner && postRunner.Action != null)
|
||||
{
|
||||
JobExecutionView snapshot;
|
||||
lock (_stateLock)
|
||||
{
|
||||
snapshot = _jobStepsSource;
|
||||
}
|
||||
|
||||
var line = snapshot?.TryClaimPredictedStep(MatchKeyFor(postRunner.Action.Id), step);
|
||||
if (line.HasValue)
|
||||
{
|
||||
Trace.Info($"DAP job steps source claimed predicted post step '{step.DisplayName}' at line {line.Value}.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Trace.Info($"DAP job steps source had no predicted line for post step '{step.DisplayName}'.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning("DAP OnPostStepRegistered error.");
|
||||
Trace.Error(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyList<JobExecutionView.PredictedPostStep> PredictPostSteps(
|
||||
IExecutionContext jobContext,
|
||||
IReadOnlyList<IStep> steps,
|
||||
IReadOnlyList<IStep> initialPostSteps)
|
||||
{
|
||||
if (jobContext == null || steps == null || steps.Count == 0)
|
||||
{
|
||||
return Array.Empty<JobExecutionView.PredictedPostStep>();
|
||||
}
|
||||
|
||||
IActionManager actionManager;
|
||||
try
|
||||
{
|
||||
actionManager = HostContext.GetService<IActionManager>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Info($"DAP post-step predictor skipped because IActionManager is unavailable ({ex.Message}).");
|
||||
return Array.Empty<JobExecutionView.PredictedPostStep>();
|
||||
}
|
||||
|
||||
var predictions = new List<JobExecutionView.PredictedPostStep>();
|
||||
var seenActionIds = new HashSet<Guid>();
|
||||
if (initialPostSteps != null)
|
||||
{
|
||||
foreach (var postStep in initialPostSteps)
|
||||
{
|
||||
if (postStep is IActionRunner postRunner && postRunner.Action != null)
|
||||
{
|
||||
seenActionIds.Add(postRunner.Action.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var step in steps)
|
||||
{
|
||||
if (step is not IActionRunner runner ||
|
||||
runner.Stage == ActionRunStage.Post ||
|
||||
runner.Action == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var action = runner.Action;
|
||||
if (action.Reference is not Pipelines.RepositoryPathReference repoRef)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!seenActionIds.Add(action.Id))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Definition definition;
|
||||
try
|
||||
{
|
||||
definition = actionManager.LoadAction(jobContext, action);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Info($"DAP post-step predictor could not load action '{repoRef.Name}' ({ex.Message}).");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (definition?.Data?.Execution?.HasPost != true)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
predictions.Add(new JobExecutionView.PredictedPostStep(
|
||||
GetPostDisplayName(runner),
|
||||
MatchKeyFor(action.Id)));
|
||||
}
|
||||
|
||||
predictions.Reverse();
|
||||
return predictions;
|
||||
}
|
||||
|
||||
private static string GetPostDisplayName(IActionRunner runner)
|
||||
{
|
||||
var displayName = string.IsNullOrEmpty(runner.DisplayName) ? "step" : runner.DisplayName;
|
||||
if (runner.Stage == ActionRunStage.Pre &&
|
||||
displayName.StartsWith("Pre ", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
displayName = displayName.Substring("Pre ".Length);
|
||||
}
|
||||
|
||||
return $"Post {displayName}";
|
||||
}
|
||||
|
||||
private static string MatchKeyFor(Guid actionId)
|
||||
{
|
||||
return $"post:{actionId:N}";
|
||||
}
|
||||
|
||||
public async Task OnJobCompletedAsync()
|
||||
{
|
||||
if (_state != DapSessionState.NotStarted)
|
||||
@@ -253,6 +433,11 @@ namespace GitHub.Runner.Worker.Dap
|
||||
if (_jobContext != null)
|
||||
{
|
||||
Trace.Info("Job completed — pausing for inspection");
|
||||
lock (_stateLock)
|
||||
{
|
||||
_jobCompleted = true;
|
||||
}
|
||||
|
||||
SendStoppedEvent("completed", "Job completed — inspect variables before the session ends.");
|
||||
|
||||
await WaitForCommandAsync(_jobContext.CancellationToken);
|
||||
@@ -359,6 +544,7 @@ namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
_state = DapSessionState.Terminated;
|
||||
}
|
||||
_jobStepsSource = null;
|
||||
}
|
||||
|
||||
_isClientConnected = false;
|
||||
@@ -417,7 +603,8 @@ namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
DisplayName = step.DisplayName,
|
||||
Result = result,
|
||||
FrameId = _nextCompletedFrameId++
|
||||
FrameId = _nextCompletedFrameId++,
|
||||
SourceLine = _jobStepsSource?.TryGetLineForStep(step)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -468,6 +655,7 @@ namespace GitHub.Runner.Worker.Dap
|
||||
"next" => HandleNext(request),
|
||||
"setBreakpoints" => HandleSetBreakpoints(request),
|
||||
"setExceptionBreakpoints" => HandleSetExceptionBreakpoints(request),
|
||||
"source" => HandleSource(request),
|
||||
"completions" => HandleCompletions(request),
|
||||
"stepIn" => CreateResponse(request, false, "Step In is not supported. Actions jobs debug at the step level - use 'next' to advance to the next step.", body: null),
|
||||
"stepOut" => CreateResponse(request, false, "Step Out is not supported. Actions jobs debug at the step level - use 'continue' to resume.", body: null),
|
||||
@@ -857,6 +1045,7 @@ namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
bool pauseOnNextStep;
|
||||
CancellationToken cancellationToken;
|
||||
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (_state != DapSessionState.Ready &&
|
||||
@@ -868,6 +1057,7 @@ namespace GitHub.Runner.Worker.Dap
|
||||
|
||||
_currentStep = step;
|
||||
_currentStepIndex = _completedSteps.Count;
|
||||
_jobCompleted = false;
|
||||
pauseOnNextStep = _pauseOnNextStep;
|
||||
cancellationToken = _jobContext?.CancellationToken ?? CancellationToken.None;
|
||||
}
|
||||
@@ -1050,29 +1240,46 @@ namespace GitHub.Runner.Worker.Dap
|
||||
private Response HandleStackTrace(Request request)
|
||||
{
|
||||
IStep currentStep;
|
||||
int currentStepIndex;
|
||||
CompletedStepInfo[] completedSteps;
|
||||
JobExecutionView jobStepsSource;
|
||||
bool jobCompleted;
|
||||
lock (_stateLock)
|
||||
{
|
||||
currentStep = _currentStep;
|
||||
currentStepIndex = _currentStepIndex;
|
||||
completedSteps = _completedSteps.ToArray();
|
||||
jobStepsSource = _jobStepsSource;
|
||||
jobCompleted = _jobCompleted;
|
||||
}
|
||||
|
||||
var frames = new List<StackFrame>();
|
||||
var source = jobStepsSource != null ? BuildJobStepsSource(jobStepsSource) : null;
|
||||
|
||||
// Add current step as the top frame
|
||||
if (currentStep != null)
|
||||
if (jobCompleted && jobStepsSource != null)
|
||||
{
|
||||
frames.Add(new StackFrame
|
||||
{
|
||||
Id = _currentFrameId,
|
||||
Name = "Complete job [completed]",
|
||||
Source = source,
|
||||
Line = jobStepsSource.CompleteJobLine,
|
||||
Column = 1,
|
||||
PresentationHint = "normal"
|
||||
});
|
||||
}
|
||||
else if (currentStep != null)
|
||||
{
|
||||
var resultIndicator = currentStep.ExecutionContext?.Result != null
|
||||
? $" [{currentStep.ExecutionContext.Result}]"
|
||||
: " [running]";
|
||||
var currentSourceLine = jobStepsSource?.TryGetLineForStep(currentStep);
|
||||
|
||||
frames.Add(new StackFrame
|
||||
{
|
||||
Id = _currentFrameId,
|
||||
Name = MaskUserVisibleText($"{currentStep.DisplayName ?? "Current Step"}{resultIndicator}"),
|
||||
Line = currentStepIndex + 1,
|
||||
Source = currentSourceLine.HasValue ? source : null,
|
||||
Line = currentSourceLine ?? 0,
|
||||
Column = 1,
|
||||
PresentationHint = "normal"
|
||||
});
|
||||
@@ -1098,7 +1305,8 @@ namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
Id = completedStep.FrameId,
|
||||
Name = MaskUserVisibleText($"{completedStep.DisplayName}{resultStr}"),
|
||||
Line = 1,
|
||||
Source = completedStep.SourceLine.HasValue ? source : null,
|
||||
Line = completedStep.SourceLine ?? 0,
|
||||
Column = 1,
|
||||
PresentationHint = "subtle"
|
||||
});
|
||||
@@ -1113,6 +1321,76 @@ namespace GitHub.Runner.Worker.Dap
|
||||
return CreateResponse(request, true, body: body);
|
||||
}
|
||||
|
||||
private Source BuildJobStepsSource(JobExecutionView snapshot)
|
||||
{
|
||||
return new Source
|
||||
{
|
||||
Name = MaskUserVisibleText(snapshot.SourceFileName),
|
||||
Path = MaskUserVisibleText($"{SanitizeSourcePathSegment(snapshot.JobId)}/{snapshot.SourceFileName}"),
|
||||
SourceReference = _jobStepsSourceReference,
|
||||
PresentationHint = "normal"
|
||||
};
|
||||
}
|
||||
|
||||
private static string SanitizeSourcePathSegment(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return "job";
|
||||
}
|
||||
|
||||
var builder = new StringBuilder(value.Length);
|
||||
foreach (var character in value)
|
||||
{
|
||||
builder.Append(char.IsControl(character) || character == '/' || character == '\\'
|
||||
? '_'
|
||||
: character);
|
||||
}
|
||||
|
||||
return builder.Length == 0 ? "job" : builder.ToString();
|
||||
}
|
||||
|
||||
internal Response HandleSource(Request request)
|
||||
{
|
||||
SourceArguments args;
|
||||
try
|
||||
{
|
||||
args = request.Arguments?.ToObject<SourceArguments>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning($"Failed to parse source arguments: {ex.GetType().Name}");
|
||||
return CreateResponse(request, false, "Invalid source arguments.", body: null);
|
||||
}
|
||||
|
||||
var sourceReference = args?.Source?.SourceReference ?? args?.SourceReference;
|
||||
if (!sourceReference.HasValue)
|
||||
{
|
||||
return CreateResponse(request, false, "Missing source reference.", body: null);
|
||||
}
|
||||
|
||||
JobExecutionView snapshot;
|
||||
lock (_stateLock)
|
||||
{
|
||||
snapshot = _jobStepsSource;
|
||||
}
|
||||
|
||||
if (snapshot == null)
|
||||
{
|
||||
return CreateResponse(request, false, "Job steps source not yet available.", body: null);
|
||||
}
|
||||
|
||||
if (sourceReference.Value != _jobStepsSourceReference)
|
||||
{
|
||||
return CreateResponse(request, false, $"Unknown source reference: {sourceReference.Value}.", body: null);
|
||||
}
|
||||
|
||||
return CreateResponse(request, true, body: new SourceResponseBody
|
||||
{
|
||||
Content = MaskUserVisibleText(snapshot.Content)
|
||||
});
|
||||
}
|
||||
|
||||
private Response HandleScopes(Request request)
|
||||
{
|
||||
var args = request.Arguments?.ToObject<ScopesArguments>();
|
||||
|
||||
@@ -537,6 +537,46 @@ namespace GitHub.Runner.Worker.Dap
|
||||
|
||||
#endregion
|
||||
|
||||
#region Source Request/Response
|
||||
|
||||
/// <summary>
|
||||
/// Arguments for 'source' request.
|
||||
/// </summary>
|
||||
public class SourceArguments
|
||||
{
|
||||
/// <summary>
|
||||
/// Source descriptor. Some clients send sourceReference only here.
|
||||
/// </summary>
|
||||
[JsonProperty("source", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public Source Source { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The reference to the source.
|
||||
/// </summary>
|
||||
[JsonProperty("sourceReference", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public int? SourceReference { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Response body for 'source' request.
|
||||
/// </summary>
|
||||
public class SourceResponseBody
|
||||
{
|
||||
/// <summary>
|
||||
/// Content of the source as a string.
|
||||
/// </summary>
|
||||
[JsonProperty("content")]
|
||||
public string Content { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional content type / mime type of the source.
|
||||
/// </summary>
|
||||
[JsonProperty("mimeType", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string MimeType { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Scopes Request/Response
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Common;
|
||||
|
||||
namespace GitHub.Runner.Worker.Dap
|
||||
@@ -19,6 +20,8 @@ namespace GitHub.Runner.Worker.Dap
|
||||
{
|
||||
Task StartAsync(IExecutionContext jobContext);
|
||||
Task WaitUntilReadyAsync();
|
||||
Task OnJobStepsInitializedAsync(IEnumerable<IStep> steps, IEnumerable<IStep> initialPostSteps);
|
||||
void OnPostStepRegistered(IStep step);
|
||||
Task OnStepStartingAsync(IStep step);
|
||||
void OnStepCompleted(IStep step);
|
||||
Task OnJobCompletedAsync();
|
||||
|
||||
@@ -362,6 +362,19 @@ namespace GitHub.Runner.Worker
|
||||
step.ExecutionContext.StepTelemetry.Action = step.DisplayName.ToLowerInvariant().Replace(' ', '_');
|
||||
}
|
||||
Root.PostJobSteps.Push(step);
|
||||
|
||||
if (Root.Global.Debugger?.Enabled == true)
|
||||
{
|
||||
try
|
||||
{
|
||||
HostContext.GetService<Dap.IDapDebugger>().OnPostStepRegistered(step);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Trace.Warning("Failed to notify DAP debugger about registered post job step.");
|
||||
Trace.Error(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public IExecutionContext CreateChild(
|
||||
|
||||
@@ -13,6 +13,7 @@ using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Runner.Worker.Dap;
|
||||
using GitHub.Services.Common;
|
||||
using GitHub.Services.WebApi;
|
||||
using Sdk.RSWebApi.Contracts;
|
||||
@@ -230,6 +231,12 @@ namespace GitHub.Runner.Worker
|
||||
jobContext.JobSteps.Enqueue(step);
|
||||
}
|
||||
|
||||
if (jobContext.Global.Debugger?.Enabled == true)
|
||||
{
|
||||
var dapDebugger = HostContext.GetService<IDapDebugger>();
|
||||
await dapDebugger.OnJobStepsInitializedAsync(jobContext.JobSteps, jobContext.PostJobSteps);
|
||||
}
|
||||
|
||||
await stepsRunner.RunAsync(jobContext);
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -11,7 +11,9 @@ using Moq;
|
||||
using GitHub.Runner.Worker;
|
||||
using GitHub.Runner.Worker.Dap;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Worker
|
||||
{
|
||||
@@ -255,6 +257,78 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
return jobContext;
|
||||
}
|
||||
|
||||
private static Mock<IStep> CreateStep(string displayName, ActionRunStage? stage = null)
|
||||
{
|
||||
var step = new Mock<IStep>();
|
||||
step.Setup(s => s.DisplayName).Returns(displayName);
|
||||
if (stage.HasValue)
|
||||
{
|
||||
var executionContext = new Mock<IExecutionContext>();
|
||||
executionContext.Setup(x => x.Stage).Returns(stage.Value);
|
||||
step.Setup(s => s.ExecutionContext).Returns(executionContext.Object);
|
||||
}
|
||||
else
|
||||
{
|
||||
step.Setup(s => s.ExecutionContext).Returns((IExecutionContext)null);
|
||||
}
|
||||
|
||||
return step;
|
||||
}
|
||||
|
||||
private static Mock<IActionRunner> CreateActionRunner(string displayName, ActionRunStage stage, Pipelines.ActionStep action)
|
||||
{
|
||||
var executionContext = new Mock<IExecutionContext>();
|
||||
executionContext.Setup(x => x.Stage).Returns(stage);
|
||||
|
||||
var runner = new Mock<IActionRunner>();
|
||||
runner.Setup(s => s.DisplayName).Returns(displayName);
|
||||
runner.Setup(s => s.ExecutionContext).Returns(executionContext.Object);
|
||||
runner.Setup(s => s.Stage).Returns(stage);
|
||||
runner.Setup(s => s.Action).Returns(action);
|
||||
return runner;
|
||||
}
|
||||
|
||||
private static Pipelines.ActionStep CreateRepositoryActionStep(string name)
|
||||
{
|
||||
return new Pipelines.ActionStep
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = name,
|
||||
Reference = new Pipelines.RepositoryPathReference
|
||||
{
|
||||
Name = name,
|
||||
Ref = "v1",
|
||||
RepositoryType = Pipelines.RepositoryTypes.GitHub
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Definition CreateActionDefinitionWithPost()
|
||||
{
|
||||
return new Definition
|
||||
{
|
||||
Data = new ActionDefinitionData
|
||||
{
|
||||
Execution = new NodeJSActionExecutionData
|
||||
{
|
||||
Script = "main.js",
|
||||
Post = "post.js"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static Request MakeRequest(string command, object arguments)
|
||||
{
|
||||
return new Request
|
||||
{
|
||||
Seq = 1,
|
||||
Type = "request",
|
||||
Command = command,
|
||||
Arguments = JObject.FromObject(arguments)
|
||||
};
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
@@ -718,6 +792,325 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task HandleSourceReturnsJobStepsSource()
|
||||
{
|
||||
using (var hc = CreateTestContext())
|
||||
{
|
||||
hc.SecretMasker.AddValue("secret-step");
|
||||
var port = GetFreePort();
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
var jobContext = CreateJobContextWithTunnel(cts.Token, port);
|
||||
await _debugger.StartAsync(jobContext.Object);
|
||||
|
||||
var waitTask = _debugger.WaitUntilReadyAsync();
|
||||
using var client = await ConnectClientAsync(port);
|
||||
var stream = client.GetStream();
|
||||
await SendRequestAsync(stream, new Request
|
||||
{
|
||||
Seq = 1,
|
||||
Type = "request",
|
||||
Command = "configurationDone"
|
||||
});
|
||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
await waitTask;
|
||||
|
||||
var pre = CreateStep("Pre cache", ActionRunStage.Pre);
|
||||
var checkout = CreateStep("Checkout");
|
||||
var secret = CreateStep("secret-step");
|
||||
var post = CreateStep("Post cache", ActionRunStage.Post);
|
||||
await _debugger.OnJobStepsInitializedAsync(
|
||||
new[] { pre.Object, checkout.Object, secret.Object },
|
||||
new[] { post.Object });
|
||||
|
||||
var response = _debugger.HandleSource(MakeRequest(
|
||||
"source",
|
||||
new SourceArguments { SourceReference = 1 }));
|
||||
|
||||
Assert.True(response.Success);
|
||||
var body = Assert.IsType<SourceResponseBody>(response.Body);
|
||||
Assert.Equal(
|
||||
"pre:\n - step: \"Set up job\"\n - step: \"Pre cache\"\n\nmain:\n - step: \"Checkout\"\n - step: \"***\"\n\npost:\n - step: \"Post cache\"\n - step: \"Complete job\"\n",
|
||||
body.Content);
|
||||
Assert.Null(body.MimeType);
|
||||
|
||||
await _debugger.StopAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task StackTraceUsesJobStepsSourceLine()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var port = GetFreePort();
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
var jobContext = CreateJobContextWithTunnel(cts.Token, port);
|
||||
await _debugger.StartAsync(jobContext.Object);
|
||||
|
||||
var waitTask = _debugger.WaitUntilReadyAsync();
|
||||
using var client = await ConnectClientAsync(port);
|
||||
var stream = client.GetStream();
|
||||
await SendRequestAsync(stream, new Request
|
||||
{
|
||||
Seq = 1,
|
||||
Type = "request",
|
||||
Command = "configurationDone"
|
||||
});
|
||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
await waitTask;
|
||||
|
||||
var checkout = CreateStep("Checkout");
|
||||
var build = CreateStep("Build");
|
||||
await _debugger.OnJobStepsInitializedAsync(
|
||||
new[] { checkout.Object, build.Object },
|
||||
Array.Empty<IStep>());
|
||||
|
||||
var stepTask = _debugger.OnStepStartingAsync(build.Object);
|
||||
var stoppedEvent = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("\"event\":\"stopped\"", stoppedEvent);
|
||||
|
||||
var bannerEvent = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("\"event\":\"output\"", bannerEvent);
|
||||
|
||||
await SendRequestAsync(stream, new Request
|
||||
{
|
||||
Seq = 2,
|
||||
Type = "request",
|
||||
Command = "stackTrace"
|
||||
});
|
||||
|
||||
var stackTraceJson = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
var stackTrace = JObject.Parse(stackTraceJson);
|
||||
var frame = stackTrace["body"]?["stackFrames"]?[0];
|
||||
|
||||
Assert.NotNull(frame);
|
||||
Assert.Equal(6, frame["line"].Value<int>());
|
||||
Assert.Equal(1, frame["source"]["sourceReference"].Value<int>());
|
||||
Assert.Equal("execution.yml", frame["source"]["name"].Value<string>());
|
||||
|
||||
await SendRequestAsync(stream, new Request
|
||||
{
|
||||
Seq = 3,
|
||||
Type = "request",
|
||||
Command = "continue"
|
||||
});
|
||||
await stepTask;
|
||||
|
||||
await _debugger.StopAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task StackTraceOmitsSourceForUnmappedCurrentStep()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var port = GetFreePort();
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
var jobContext = CreateJobContextWithTunnel(cts.Token, port);
|
||||
await _debugger.StartAsync(jobContext.Object);
|
||||
|
||||
var waitTask = _debugger.WaitUntilReadyAsync();
|
||||
using var client = await ConnectClientAsync(port);
|
||||
var stream = client.GetStream();
|
||||
await SendRequestAsync(stream, new Request
|
||||
{
|
||||
Seq = 1,
|
||||
Type = "request",
|
||||
Command = "configurationDone"
|
||||
});
|
||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
await waitTask;
|
||||
|
||||
var checkout = CreateStep("Checkout");
|
||||
var build = CreateStep("Build");
|
||||
await _debugger.OnJobStepsInitializedAsync(
|
||||
new[] { checkout.Object },
|
||||
Array.Empty<IStep>());
|
||||
|
||||
var stepTask = _debugger.OnStepStartingAsync(build.Object);
|
||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
|
||||
await SendRequestAsync(stream, new Request
|
||||
{
|
||||
Seq = 2,
|
||||
Type = "request",
|
||||
Command = "stackTrace"
|
||||
});
|
||||
|
||||
var stackTraceJson = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
var stackTrace = JObject.Parse(stackTraceJson);
|
||||
var frame = stackTrace["body"]?["stackFrames"]?[0];
|
||||
|
||||
Assert.NotNull(frame);
|
||||
Assert.Equal(0, frame["line"].Value<int>());
|
||||
Assert.Null(frame["source"]);
|
||||
|
||||
await SendRequestAsync(stream, new Request
|
||||
{
|
||||
Seq = 3,
|
||||
Type = "request",
|
||||
Command = "continue"
|
||||
});
|
||||
await stepTask;
|
||||
|
||||
await _debugger.StopAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task PredictedPostStepIsServedAtInitializationAndClaimedAtRegistration()
|
||||
{
|
||||
using (var hc = CreateTestContext())
|
||||
{
|
||||
var action = CreateRepositoryActionStep("actions/cache");
|
||||
var actionManager = new Mock<IActionManager>();
|
||||
actionManager
|
||||
.Setup(x => x.LoadAction(It.IsAny<IExecutionContext>(), action))
|
||||
.Returns(CreateActionDefinitionWithPost());
|
||||
hc.SetSingleton<IActionManager>(actionManager.Object);
|
||||
|
||||
var port = GetFreePort();
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
var jobContext = CreateJobContextWithTunnel(cts.Token, port);
|
||||
await _debugger.StartAsync(jobContext.Object);
|
||||
|
||||
var waitTask = _debugger.WaitUntilReadyAsync();
|
||||
using var client = await ConnectClientAsync(port);
|
||||
var stream = client.GetStream();
|
||||
await SendRequestAsync(stream, new Request
|
||||
{
|
||||
Seq = 1,
|
||||
Type = "request",
|
||||
Command = "configurationDone"
|
||||
});
|
||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
await waitTask;
|
||||
|
||||
var checkout = CreateActionRunner("Checkout", ActionRunStage.Main, action);
|
||||
await _debugger.OnJobStepsInitializedAsync(
|
||||
new[] { checkout.Object },
|
||||
Array.Empty<IStep>());
|
||||
|
||||
var sourceResponse = _debugger.HandleSource(MakeRequest(
|
||||
"source",
|
||||
new SourceArguments { SourceReference = 1 }));
|
||||
var sourceBody = Assert.IsType<SourceResponseBody>(sourceResponse.Body);
|
||||
Assert.Equal(
|
||||
"pre:\n - step: \"Set up job\"\n\nmain:\n - step: \"Checkout\"\n\npost:\n - step: \"Post Checkout\"\n - step: \"Complete job\"\n",
|
||||
sourceBody.Content);
|
||||
|
||||
var post = CreateActionRunner("Post Checkout", ActionRunStage.Post, action);
|
||||
_debugger.OnPostStepRegistered(post.Object);
|
||||
|
||||
var stepTask = _debugger.OnStepStartingAsync(post.Object);
|
||||
var stoppedEvent = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("\"event\":\"stopped\"", stoppedEvent);
|
||||
|
||||
var bannerEvent = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("\"event\":\"output\"", bannerEvent);
|
||||
|
||||
await SendRequestAsync(stream, new Request
|
||||
{
|
||||
Seq = 2,
|
||||
Type = "request",
|
||||
Command = "stackTrace"
|
||||
});
|
||||
|
||||
var stackTraceJson = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
var stackTrace = JObject.Parse(stackTraceJson);
|
||||
var frame = stackTrace["body"]?["stackFrames"]?[0];
|
||||
|
||||
Assert.NotNull(frame);
|
||||
Assert.Equal(8, frame["line"].Value<int>());
|
||||
Assert.Equal(1, frame["source"]["sourceReference"].Value<int>());
|
||||
|
||||
await SendRequestAsync(stream, new Request
|
||||
{
|
||||
Seq = 3,
|
||||
Type = "request",
|
||||
Command = "continue"
|
||||
});
|
||||
await stepTask;
|
||||
|
||||
await _debugger.StopAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task StackTraceSanitizesSyntheticSourcePath()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var port = GetFreePort();
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
var jobContext = CreateJobContextWithTunnel(cts.Token, port, jobName: "my/job\\name");
|
||||
await _debugger.StartAsync(jobContext.Object);
|
||||
|
||||
var waitTask = _debugger.WaitUntilReadyAsync();
|
||||
using var client = await ConnectClientAsync(port);
|
||||
var stream = client.GetStream();
|
||||
await SendRequestAsync(stream, new Request
|
||||
{
|
||||
Seq = 1,
|
||||
Type = "request",
|
||||
Command = "configurationDone"
|
||||
});
|
||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
await waitTask;
|
||||
|
||||
var checkout = CreateStep("Checkout");
|
||||
await _debugger.OnJobStepsInitializedAsync(
|
||||
new[] { checkout.Object },
|
||||
Array.Empty<IStep>());
|
||||
|
||||
var stepTask = _debugger.OnStepStartingAsync(checkout.Object);
|
||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
|
||||
await SendRequestAsync(stream, new Request
|
||||
{
|
||||
Seq = 2,
|
||||
Type = "request",
|
||||
Command = "stackTrace"
|
||||
});
|
||||
|
||||
var stackTraceJson = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
var stackTrace = JObject.Parse(stackTraceJson);
|
||||
var frame = stackTrace["body"]?["stackFrames"]?[0];
|
||||
|
||||
Assert.NotNull(frame);
|
||||
Assert.Equal("my_job_name/execution.yml", frame["source"]["path"].Value<string>());
|
||||
|
||||
await SendRequestAsync(stream, new Request
|
||||
{
|
||||
Seq = 3,
|
||||
Type = "request",
|
||||
Command = "continue"
|
||||
});
|
||||
await stepTask;
|
||||
|
||||
await _debugger.StopAsync();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
@@ -746,6 +1139,11 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
await waitTask;
|
||||
|
||||
var checkout = CreateStep("Checkout");
|
||||
await _debugger.OnJobStepsInitializedAsync(
|
||||
new[] { checkout.Object },
|
||||
Array.Empty<IStep>());
|
||||
|
||||
// Complete the job — OnJobCompletedAsync pauses when stepping,
|
||||
// so run it in the background and send continue to unblock.
|
||||
var completedTask = _debugger.OnJobCompletedAsync();
|
||||
@@ -754,11 +1152,27 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
var stoppedMsg = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
Assert.Contains("\"event\":\"stopped\"", stoppedMsg);
|
||||
|
||||
// Send continue to unblock the pause
|
||||
await SendRequestAsync(stream, new Request
|
||||
{
|
||||
Seq = 2,
|
||||
Type = "request",
|
||||
Command = "stackTrace"
|
||||
});
|
||||
|
||||
var stackTraceJson = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
var stackTrace = JObject.Parse(stackTraceJson);
|
||||
var frame = stackTrace["body"]?["stackFrames"]?[0];
|
||||
|
||||
Assert.NotNull(frame);
|
||||
Assert.Equal("Complete job [completed]", frame["name"].Value<string>());
|
||||
Assert.Equal(8, frame["line"].Value<int>());
|
||||
Assert.Equal(1, frame["source"]["sourceReference"].Value<int>());
|
||||
|
||||
// Send continue to unblock the pause
|
||||
await SendRequestAsync(stream, new Request
|
||||
{
|
||||
Seq = 3,
|
||||
Type = "request",
|
||||
Command = "continue"
|
||||
});
|
||||
|
||||
@@ -777,6 +1191,68 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public async Task OnJobCompletedUsesSyntheticCompleteJobLineWhenPostStepSharesName()
|
||||
{
|
||||
using (CreateTestContext())
|
||||
{
|
||||
var port = GetFreePort();
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||
var jobContext = CreateJobContextWithTunnel(cts.Token, port);
|
||||
await _debugger.StartAsync(jobContext.Object);
|
||||
|
||||
var waitTask = _debugger.WaitUntilReadyAsync();
|
||||
using var client = await ConnectClientAsync(port);
|
||||
var stream = client.GetStream();
|
||||
await SendRequestAsync(stream, new Request
|
||||
{
|
||||
Seq = 1,
|
||||
Type = "request",
|
||||
Command = "configurationDone"
|
||||
});
|
||||
|
||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
await waitTask;
|
||||
|
||||
var checkout = CreateStep("Checkout");
|
||||
var realPost = CreateStep("Complete job", ActionRunStage.Post);
|
||||
await _debugger.OnJobStepsInitializedAsync(
|
||||
new[] { checkout.Object },
|
||||
new[] { realPost.Object });
|
||||
|
||||
var completedTask = _debugger.OnJobCompletedAsync();
|
||||
|
||||
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
|
||||
await SendRequestAsync(stream, new Request
|
||||
{
|
||||
Seq = 2,
|
||||
Type = "request",
|
||||
Command = "stackTrace"
|
||||
});
|
||||
|
||||
var stackTraceJson = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
|
||||
var stackTrace = JObject.Parse(stackTraceJson);
|
||||
var frame = stackTrace["body"]?["stackFrames"]?[0];
|
||||
|
||||
Assert.NotNull(frame);
|
||||
Assert.Equal("Complete job [completed]", frame["name"].Value<string>());
|
||||
Assert.Equal(9, frame["line"].Value<int>());
|
||||
|
||||
await SendRequestAsync(stream, new Request
|
||||
{
|
||||
Seq = 3,
|
||||
Type = "request",
|
||||
Command = "continue"
|
||||
});
|
||||
|
||||
await completedTask;
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Xunit;
|
||||
@@ -171,6 +171,36 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
Assert.Equal("normal", deserialized.PresentationHint);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void SourceRequestAndResponseSerialization()
|
||||
{
|
||||
var args = new SourceArguments
|
||||
{
|
||||
Source = new Source
|
||||
{
|
||||
SourceReference = 1
|
||||
}
|
||||
};
|
||||
|
||||
var argsJson = JsonConvert.SerializeObject(args);
|
||||
var deserializedArgs = JsonConvert.DeserializeObject<SourceArguments>(argsJson);
|
||||
|
||||
Assert.Equal(1, deserializedArgs.Source.SourceReference);
|
||||
|
||||
var body = new SourceResponseBody
|
||||
{
|
||||
Content = "pre:\n - step: \"Setup job\"\n\nmain:\n - step: \"Checkout\"\n\npost:\n - step: \"Complete job\"\n"
|
||||
};
|
||||
|
||||
var bodyJson = JsonConvert.SerializeObject(body);
|
||||
var deserializedBody = JsonConvert.DeserializeObject<SourceResponseBody>(bodyJson);
|
||||
|
||||
Assert.Equal("pre:\n - step: \"Setup job\"\n\nmain:\n - step: \"Checkout\"\n\npost:\n - step: \"Complete job\"\n", deserializedBody.Content);
|
||||
Assert.Null(deserializedBody.MimeType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
|
||||
Reference in New Issue
Block a user