Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
bcab143723 Bump System.ServiceProcess.ServiceController from 10.0.3 to 10.0.8
---
updated-dependencies:
- dependency-name: System.ServiceProcess.ServiceController
  dependency-version: 10.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-18 13:35:38 +00:00
23 changed files with 76 additions and 1973 deletions

3
.gitignore vendored
View File

@@ -27,5 +27,4 @@ TestResults
TestLogs
.DS_Store
.mono
**/*.DotSettings.user
**/*.lscache
**/*.DotSettings.user

View File

@@ -5,8 +5,8 @@ ARG TARGETOS
ARG TARGETARCH
ARG RUNNER_VERSION
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0
ARG DOCKER_VERSION=29.5.0
ARG BUILDX_VERSION=0.34.0
ARG DOCKER_VERSION=29.4.0
ARG BUILDX_VERSION=0.33.0
RUN apt update -y && apt install curl unzip -y

View File

@@ -7,7 +7,7 @@ NODE_ALPINE_URL=https://github.com/actions/alpine_nodejs/releases/download
# When you update Node versions you must also create a new release of alpine_nodejs at that updated version.
# Follow the instructions here: https://github.com/actions/alpine_nodejs?tab=readme-ov-file#getting-started
NODE20_VERSION="20.20.2"
NODE24_VERSION="24.16.0"
NODE24_VERSION="24.15.0"
get_abs_path() {
# exploits the fact that pwd will print abs path when no args

View File

@@ -179,7 +179,6 @@ namespace GitHub.Runner.Common
public static readonly string EmitCompositeMarkers = "actions_runner_emit_composite_markers";
public static readonly string BatchActionResolution = "actions_batch_action_resolution";
public static readonly string UseBearerTokenForCodeload = "actions_use_bearer_token_for_codeload";
public static readonly string OverrideDebuggerWelcomeMessage = "actions_runner_override_debugger_welcome_message";
}
// Node version migration related constants
@@ -206,7 +205,7 @@ namespace GitHub.Runner.Common
public static readonly string Node20DeprecationUrl = "https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/";
// Node 20 migration dates (hardcoded fallbacks, can be overridden via job variables)
public static readonly string Node24DefaultDate = "June 16th, 2026";
public static readonly string Node24DefaultDate = "June 2nd, 2026";
public static readonly string Node20RemovalDate = "September 16th, 2026";
// Variable keys for server-overridable dates

View File

@@ -16,7 +16,6 @@ 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
{
@@ -28,7 +27,6 @@ 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>
@@ -56,9 +54,6 @@ 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;
@@ -68,7 +63,6 @@ namespace GitHub.Runner.Worker.Dap
private volatile DapSessionState _state = DapSessionState.NotStarted;
private CancellationTokenRegistration? _cancellationRegistration;
private bool _isFirstStep = true;
private bool _welcomeMessageSent;
// Dev Tunnel relay host for remote debugging
private TunnelRelayTunnelHost _tunnelRelayHost;
@@ -103,8 +97,6 @@ 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;
@@ -247,179 +239,6 @@ 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)
@@ -433,11 +252,6 @@ 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);
@@ -544,7 +358,6 @@ namespace GitHub.Runner.Worker.Dap
{
_state = DapSessionState.Terminated;
}
_jobStepsSource = null;
}
_isClientConnected = false;
@@ -603,8 +416,7 @@ namespace GitHub.Runner.Worker.Dap
{
DisplayName = step.DisplayName,
Result = result,
FrameId = _nextCompletedFrameId++,
SourceLine = _jobStepsSource?.TryGetLineForStep(step)
FrameId = _nextCompletedFrameId++
});
}
}
@@ -655,7 +467,6 @@ 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),
@@ -679,11 +490,6 @@ namespace GitHub.Runner.Worker.Dap
});
Trace.Info("Sent initialized event");
}
if (request.Command == "configurationDone")
{
SendWelcomeMessage();
}
}
catch (Exception ex)
{
@@ -702,7 +508,6 @@ namespace GitHub.Runner.Worker.Dap
internal void HandleClientConnected()
{
_isClientConnected = true;
_welcomeMessageSent = false;
Trace.Info("Client connected to debug session");
// If we're paused, re-send the stopped event so the new client
@@ -1013,39 +818,10 @@ namespace GitHub.Runner.Worker.Dap
});
}
internal void SendWelcomeMessage()
{
if (_welcomeMessageSent)
{
return;
}
_welcomeMessageSent = true;
var debuggerConfig = _jobContext?.Global?.Debugger;
if (debuggerConfig?.OverrideWelcomeMessage == true)
{
if (!string.IsNullOrEmpty(debuggerConfig.WelcomeMessage))
{
SendOutput("console", debuggerConfig.WelcomeMessage);
Trace.Info("Sent custom welcome message");
}
else
{
Trace.Info("Welcome message suppressed by override");
}
}
else
{
SendOutput("console", DapReplParser.GetGeneralHelp());
Trace.Info("Sent default welcome message");
}
}
internal async Task OnStepStartingAsync(IStep step, bool isFirstStep)
{
bool pauseOnNextStep;
CancellationToken cancellationToken;
lock (_stateLock)
{
if (_state != DapSessionState.Ready &&
@@ -1057,7 +833,6 @@ namespace GitHub.Runner.Worker.Dap
_currentStep = step;
_currentStepIndex = _completedSteps.Count;
_jobCompleted = false;
pauseOnNextStep = _pauseOnNextStep;
cancellationToken = _jobContext?.CancellationToken ?? CancellationToken.None;
}
@@ -1085,9 +860,6 @@ namespace GitHub.Runner.Worker.Dap
// Send stopped event to debugger (only if client is connected)
SendStoppedEvent(reason, description);
// Emit a banner so the user knows where REPL commands will execute
SendExecutionContextBanner();
// Wait for debugger command
await WaitForCommandAsync(cancellationToken);
}
@@ -1240,46 +1012,29 @@ 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 (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)
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}"),
Source = currentSourceLine.HasValue ? source : null,
Line = currentSourceLine ?? 0,
Line = currentStepIndex + 1,
Column = 1,
PresentationHint = "normal"
});
@@ -1305,8 +1060,7 @@ namespace GitHub.Runner.Worker.Dap
{
Id = completedStep.FrameId,
Name = MaskUserVisibleText($"{completedStep.DisplayName}{resultStr}"),
Source = completedStep.SourceLine.HasValue ? source : null,
Line = completedStep.SourceLine ?? 0,
Line = 1,
Column = 1,
PresentationHint = "subtle"
});
@@ -1321,76 +1075,6 @@ 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>();
@@ -1511,12 +1195,7 @@ namespace GitHub.Runner.Worker.Dap
case RunCommand run:
var context = GetExecutionContextForFrame(frameId);
bool isActionStep;
lock (_stateLock)
{
isActionStep = _currentStep is IActionRunner;
}
return await _replExecutor.ExecuteRunCommandAsync(run, context, isActionStep, cancellationToken);
return await _replExecutor.ExecuteRunCommandAsync(run, context, cancellationToken);
default:
return new EvaluateResponseBody
@@ -1728,40 +1407,6 @@ namespace GitHub.Runner.Worker.Dap
});
}
/// <summary>
/// Emits a console output banner telling the user whether REPL
/// commands will execute on the host or inside the job container.
/// </summary>
private void SendExecutionContextBanner()
{
if (!_isClientConnected)
{
return;
}
bool isActionStep = _currentStep is IActionRunner;
var container = _jobContext?.Global?.Container;
string target;
if (isActionStep && container != null &&
(!string.IsNullOrEmpty(container.ContainerId) ||
FeatureManager.IsContainerHooksEnabled(_jobContext?.Global?.Variables)))
{
var image = container.ContainerImage ?? "container";
var shortId = !string.IsNullOrEmpty(container.ContainerId) && container.ContainerId.Length >= 12
? container.ContainerId.Substring(0, 12)
: container.ContainerId ?? "";
var idSuffix = !string.IsNullOrEmpty(shortId) ? $" ({shortId})" : "";
target = $"job container: {image}{idSuffix}";
}
else
{
target = "runner host";
}
SendOutput("console", $"\nCommands will run on {target}\n");
}
private string MaskUserVisibleText(string value)
{
if (string.IsNullOrEmpty(value))

View File

@@ -537,46 +537,6 @@ 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>

View File

@@ -9,7 +9,6 @@ using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Handlers;
namespace GitHub.Runner.Worker.Dap
@@ -44,7 +43,6 @@ namespace GitHub.Runner.Worker.Dap
public async Task<EvaluateResponseBody> ExecuteRunCommandAsync(
RunCommand command,
IExecutionContext context,
bool isActionStep,
CancellationToken cancellationToken)
{
if (context == null)
@@ -54,7 +52,7 @@ namespace GitHub.Runner.Worker.Dap
try
{
return await ExecuteScriptAsync(command, context, isActionStep, cancellationToken);
return await ExecuteScriptAsync(command, context, cancellationToken);
}
catch (Exception ex)
{
@@ -67,17 +65,9 @@ namespace GitHub.Runner.Worker.Dap
private async Task<EvaluateResponseBody> ExecuteScriptAsync(
RunCommand command,
IExecutionContext context,
bool isActionStep,
CancellationToken cancellationToken)
{
// 1. Resolve step host — container or host, same as ActionRunner.
// Only action steps (user-defined run:/uses:) execute inside the
// container. Infrastructure steps (Set up job, Initialize
// containers, Complete job, etc.) always run on the host.
var stepHost = CreateStepHost(context, isActionStep);
var isContainerStepHost = stepHost is IContainerStepHost;
// 2. Resolve shell — same logic as ScriptHandler
// 1. Resolve shell — same logic as ScriptHandler
string shellCommand;
string argFormat;
@@ -97,9 +87,9 @@ namespace GitHub.Runner.Worker.Dap
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
}
_trace.Info($"Resolved REPL shell (container={isContainerStepHost})");
_trace.Info("Resolved REPL shell");
// 3. Expand ${{ }} expressions in the script body, just like
// 2. Expand ${{ }} expressions in the script body, just like
// ActionRunner evaluates step inputs before ScriptHandler sees them
var contents = ExpandExpressions(command.Script, context);
contents = ScriptHandlerHelpers.FixUpScriptContents(shellCommand, contents);
@@ -121,47 +111,25 @@ namespace GitHub.Runner.Worker.Dap
try
{
// 4. Resolve script path — translate for container if needed
var resolvedPath = stepHost.ResolvePathForStepHost(context, scriptFilePath).Replace("\"", "\\\"");
// 3. Format arguments with script path
var resolvedPath = scriptFilePath.Replace("\"", "\\\"");
if (string.IsNullOrEmpty(argFormat) || !argFormat.Contains("{0}"))
{
return ErrorResult($"Invalid shell option '{shellCommand}'. Shell must be a valid built-in (bash, sh, cmd, powershell, pwsh) or a format string containing '{{0}}'");
}
var arguments = string.Format(argFormat, resolvedPath);
// 5. Resolve shell command path — for containers, use the shell
// name directly (it will be resolved inside the container);
// for host execution, resolve the full path on the host.
// 4. Resolve shell command path
string prependPath = string.Join(
Path.PathSeparator.ToString(),
Enumerable.Reverse(context.Global.PrependPath));
var fileName = isContainerStepHost
? shellCommand
: WhichUtil.Which(shellCommand, false, _trace, prependPath) ?? shellCommand;
var commandPath = WhichUtil.Which(shellCommand, false, _trace, prependPath)
?? shellCommand;
// 6. Build environment — merge from execution context like a real step
// 5. Build environment — merge from execution context like a real step
var environment = BuildEnvironment(context, command.Env);
// 7. Handle PrependPath — mirrors Handler.AddPrependPathToEnvironment
if (context.Global.PrependPath.Count > 0)
{
if (stepHost is IContainerStepHost containerHost)
{
containerHost.PrependPath = prependPath;
}
else
{
string taskEnvPATH;
environment.TryGetValue(Constants.PathVariable, out taskEnvPATH);
string originalPath = context.Global.Variables?.Get(Constants.PathVariable) ?? // Prefer a job variable.
taskEnvPATH ?? // Then a task-environment variable.
System.Environment.GetEnvironmentVariable(Constants.PathVariable) ?? // Then an environment variable.
string.Empty;
environment[Constants.PathVariable] = PathUtil.PrependPath(prependPath, originalPath);
}
}
// 8. Resolve working directory — translate for container
// 6. Resolve working directory
var workingDirectory = command.WorkingDirectory;
if (string.IsNullOrEmpty(workingDirectory))
{
@@ -173,60 +141,48 @@ namespace GitHub.Runner.Worker.Dap
: null;
workingDirectory = workspace ?? _hostContext.GetDirectory(WellKnownDirectory.Work);
}
workingDirectory = stepHost.ResolvePathForStepHost(context, workingDirectory);
_trace.Info("Executing REPL command");
// Stream execution info to debugger
SendOutput("console", $"$ {shellCommand} {command.Script.Substring(0, Math.Min(command.Script.Length, 80))}{(command.Script.Length > 80 ? "..." : "")}\n");
// NOTE: When container hooks are enabled, ContainerStepHost routes
// execution through IContainerHookManager which does not raise
// OutputDataReceived/ErrorDataReceived events. Output will not be
// streamed to the debug console in that mode.
if (isContainerStepHost && FeatureManager.IsContainerHooksEnabled(context.Global?.Variables))
// 7. Execute via IProcessInvoker (same as DefaultStepHost)
int exitCode;
using (var processInvoker = _hostContext.CreateService<IProcessInvoker>())
{
const string hookWarning = "Container hooks are enabled. REPL output will not be streamed to the debug console for this command.";
_trace.Warning(hookWarning);
SendOutput("stderr", hookWarning + "\n");
processInvoker.OutputDataReceived += (sender, args) =>
{
if (!string.IsNullOrEmpty(args.Data))
{
var masked = _hostContext.SecretMasker.MaskSecrets(args.Data);
SendOutput("stdout", masked + "\n");
}
};
processInvoker.ErrorDataReceived += (sender, args) =>
{
if (!string.IsNullOrEmpty(args.Data))
{
var masked = _hostContext.SecretMasker.MaskSecrets(args.Data);
SendOutput("stderr", masked + "\n");
}
};
exitCode = await processInvoker.ExecuteAsync(
workingDirectory: workingDirectory,
fileName: commandPath,
arguments: arguments,
environment: environment,
requireExitCodeZero: false,
outputEncoding: null,
killProcessOnCancel: true,
cancellationToken: cancellationToken);
}
// 9. Execute via IStepHost — handles docker exec for containers,
// direct process execution for host, and container hooks
stepHost.OutputDataReceived += (sender, args) =>
{
if (!string.IsNullOrEmpty(args.Data))
{
var masked = _hostContext.SecretMasker.MaskSecrets(args.Data);
SendOutput("stdout", masked + "\n");
}
};
stepHost.ErrorDataReceived += (sender, args) =>
{
if (!string.IsNullOrEmpty(args.Data))
{
var masked = _hostContext.SecretMasker.MaskSecrets(args.Data);
SendOutput("stderr", masked + "\n");
}
};
int exitCode = await stepHost.ExecuteAsync(
context: context,
workingDirectory: workingDirectory,
fileName: fileName,
arguments: arguments,
environment: environment,
requireExitCodeZero: false,
outputEncoding: null,
killProcessOnCancel: true,
inheritConsoleHandler: false,
standardInInput: null,
cancellationToken: cancellationToken);
_trace.Info($"REPL command exited with code {exitCode}");
// 10. Return only the exit code summary (output was already streamed)
// 8. Return only the exit code summary (output was already streamed)
return new EvaluateResponseBody
{
Result = exitCode == 0 ? $"(exit code: {exitCode})" : $"Process completed with exit code {exitCode}.",
@@ -242,43 +198,6 @@ namespace GitHub.Runner.Worker.Dap
}
}
/// <summary>
/// Creates the appropriate <see cref="IStepHost"/> for the current
/// execution context, mirroring how <see cref="ActionRunner"/> decides
/// between host and container execution.
///
/// Only action steps (user-defined run:/uses: steps) run inside the
/// job container. Infrastructure steps like "Set up job", "Initialize
/// containers", "Stop containers", and "Complete job" always execute
/// on the host regardless of whether a container is configured.
/// </summary>
internal IStepHost CreateStepHost(IExecutionContext context, bool isActionStep)
{
if (!isActionStep)
{
_trace.Info("Creating DefaultStepHost for REPL execution (infrastructure step)");
return _hostContext.CreateService<IDefaultStepHost>();
}
var container = context?.Global?.Container;
if (container != null)
{
// Container hooks don't always set ContainerId, but the container
// step host handles that internally
var hooksEnabled = FeatureManager.IsContainerHooksEnabled(context.Global?.Variables);
if (hooksEnabled || !string.IsNullOrEmpty(container.ContainerId))
{
_trace.Info("Creating ContainerStepHost for REPL execution");
var containerStepHost = _hostContext.CreateService<IContainerStepHost>();
containerStepHost.Container = container;
return containerStepHost;
}
}
_trace.Info("Creating DefaultStepHost for REPL execution");
return _hostContext.CreateService<IDefaultStepHost>();
}
/// <summary>
/// Expands <c>${{ }}</c> expressions in the input string using the
/// runner's template evaluator — the same evaluation path that processes

View File

@@ -1,4 +1,4 @@
using GitHub.DistributedTask.Pipelines;
using GitHub.DistributedTask.Pipelines;
namespace GitHub.Runner.Worker.Dap
{
@@ -8,12 +8,10 @@ namespace GitHub.Runner.Worker.Dap
/// </summary>
public sealed class DebuggerConfig
{
public DebuggerConfig(bool enabled, DebuggerTunnelInfo tunnel, bool overrideWelcomeMessage = false, string welcomeMessage = null)
public DebuggerConfig(bool enabled, DebuggerTunnelInfo tunnel)
{
Enabled = enabled;
Tunnel = tunnel;
OverrideWelcomeMessage = overrideWelcomeMessage;
WelcomeMessage = welcomeMessage;
}
/// <summary>Whether the debugger is enabled for this job.</summary>
@@ -25,19 +23,6 @@ namespace GitHub.Runner.Worker.Dap
/// </summary>
public DebuggerTunnelInfo Tunnel { get; }
/// <summary>
/// When true, the runner overrides the default welcome message with
/// <see cref="WelcomeMessage"/>. A null or empty <see cref="WelcomeMessage"/>
/// suppresses the message entirely. When false, the default help text is shown.
/// </summary>
public bool OverrideWelcomeMessage { get; }
/// <summary>
/// Optional welcome message content for the debugger console. Only used when
/// <see cref="OverrideWelcomeMessage"/> is true.
/// </summary>
public string WelcomeMessage { get; }
/// <summary>Whether the tunnel configuration is complete and valid.</summary>
public bool HasValidTunnel => Tunnel != null
&& !string.IsNullOrEmpty(Tunnel.TunnelId)

View File

@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Threading.Tasks;
using GitHub.Runner.Common;
namespace GitHub.Runner.Worker.Dap
@@ -20,8 +19,6 @@ 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();

View File

@@ -1,358 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
namespace GitHub.Runner.Worker.Dap
{
internal sealed class JobExecutionView
{
private const string _sourceFileName = "execution.yml";
private readonly object _lock = new object();
private readonly List<SourceEntry> _preEntries = new List<SourceEntry>();
private readonly List<SourceEntry> _mainEntries = new List<SourceEntry>();
private readonly List<SourceEntry> _postEntries = new List<SourceEntry>();
private readonly List<StepLine> _lineByStep = new List<StepLine>();
private string _content;
private int _completeJobLine;
public JobExecutionView(
string jobId,
IEnumerable<IStep> steps,
IEnumerable<IStep> initialPostSteps,
IEnumerable<PredictedPostStep> predictedPostSteps = null)
{
JobId = string.IsNullOrWhiteSpace(jobId) ? "job" : jobId;
_preEntries.Add(new SourceEntry("Setup job"));
AddSteps(steps);
AddPredictedPostSteps(predictedPostSteps);
AddSteps(initialPostSteps);
_postEntries.Add(SourceEntry.CreateSyntheticCompleteJob());
Render();
}
public string JobId { get; }
public string SourceFileName => _sourceFileName;
public string Content
{
get
{
lock (_lock)
{
return _content;
}
}
}
public int CompleteJobLine
{
get
{
lock (_lock)
{
return _completeJobLine;
}
}
}
public int? TryClaimPredictedStep(string matchKey, IStep step)
{
if (string.IsNullOrEmpty(matchKey) || step == null)
{
return null;
}
lock (_lock)
{
var existingLine = TryGetLineForStepNoLock(step);
if (existingLine.HasValue)
{
return existingLine;
}
foreach (var entry in _postEntries)
{
if (!string.Equals(entry.MatchKey, matchKey, StringComparison.Ordinal))
{
continue;
}
if (entry.Step != null && !ReferenceEquals(entry.Step, step))
{
return null;
}
entry.Step = step;
Render();
return TryGetLineForStepNoLock(step);
}
return null;
}
}
public int? TryGetLineForStep(IStep step)
{
if (step == null)
{
return null;
}
lock (_lock)
{
return TryGetLineForStepNoLock(step);
}
}
private int? TryGetLineForStepNoLock(IStep step)
{
foreach (var stepLine in _lineByStep)
{
if (ReferenceEquals(stepLine.Step, step))
{
return stepLine.Line;
}
}
return null;
}
private void AddSteps(IEnumerable<IStep> steps)
{
if (steps == null)
{
return;
}
foreach (var step in steps)
{
if (step == null)
{
continue;
}
GetEntries(GetSection(step)).Add(new SourceEntry(step));
}
}
private void AddPredictedPostSteps(IEnumerable<PredictedPostStep> steps)
{
if (steps == null)
{
return;
}
foreach (var step in steps)
{
if (step == null)
{
continue;
}
_postEntries.Add(new SourceEntry(step.DisplayName, step.MatchKey));
}
}
private List<SourceEntry> GetEntries(SourceSection section)
{
switch (section)
{
case SourceSection.Pre:
return _preEntries;
case SourceSection.Post:
return _postEntries;
default:
return _mainEntries;
}
}
private static SourceSection GetSection(IStep step)
{
if (step is IActionRunner actionRunner)
{
return GetSection(actionRunner.Stage);
}
if (step.ExecutionContext != null)
{
return GetSection(step.ExecutionContext.Stage);
}
return SourceSection.Main;
}
private static SourceSection GetSection(ActionRunStage stage)
{
switch (stage)
{
case ActionRunStage.Pre:
return SourceSection.Pre;
case ActionRunStage.Post:
return SourceSection.Post;
default:
return SourceSection.Main;
}
}
private void Render()
{
_lineByStep.Clear();
_completeJobLine = 0;
var sb = new StringBuilder();
var line = 1;
AppendSection(sb, "pre", _preEntries, ref line, appendSeparatorLine: true);
AppendSection(sb, "main", _mainEntries, ref line, appendSeparatorLine: true);
AppendSection(sb, "post", _postEntries, ref line, appendSeparatorLine: false);
_content = sb.ToString();
}
private void AppendSection(
StringBuilder sb,
string sectionName,
IReadOnlyList<SourceEntry> entries,
ref int line,
bool appendSeparatorLine)
{
sb.Append(sectionName).Append(":\n");
line++;
foreach (var entry in entries)
{
if (entry.Step != null && TryGetLineForStepNoLock(entry.Step) == null)
{
_lineByStep.Add(new StepLine(entry.Step, line));
}
sb.Append(" - step: ");
sb.Append(FormatYamlString(entry.DisplayName));
sb.Append('\n');
if (entry.IsSyntheticCompleteJob)
{
_completeJobLine = line;
}
line++;
}
if (appendSeparatorLine)
{
sb.Append('\n');
line++;
}
}
private static string FormatYamlString(string value)
{
var sb = new StringBuilder();
sb.Append('"');
foreach (var c in value)
{
switch (c)
{
case '\\':
sb.Append(@"\\");
break;
case '"':
sb.Append("\\\"");
break;
case '\r':
sb.Append(@"\r");
break;
case '\n':
sb.Append(@"\n");
break;
case '\t':
sb.Append(@"\t");
break;
default:
if (char.IsControl(c))
{
sb.Append(@"\u");
sb.Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
}
else
{
sb.Append(c);
}
break;
}
}
sb.Append('"');
return sb.ToString();
}
internal sealed class PredictedPostStep
{
public PredictedPostStep(string displayName, string matchKey)
{
DisplayName = string.IsNullOrEmpty(displayName) ? "step" : displayName;
MatchKey = matchKey;
}
public string DisplayName { get; }
public string MatchKey { get; }
}
private sealed class StepLine
{
public StepLine(IStep step, int line)
{
Step = step;
Line = line;
}
public IStep Step { get; }
public int Line { get; }
}
private sealed class SourceEntry
{
public SourceEntry(string displayName)
{
DisplayName = string.IsNullOrEmpty(displayName) ? "step" : displayName;
}
public SourceEntry(string displayName, string matchKey)
: this(displayName)
{
MatchKey = matchKey;
}
public SourceEntry(IStep step)
{
Step = step;
DisplayName = string.IsNullOrEmpty(step.DisplayName) ? "step" : step.DisplayName;
}
private SourceEntry(string displayName, bool isSyntheticCompleteJob)
: this(displayName)
{
IsSyntheticCompleteJob = isSyntheticCompleteJob;
}
public static SourceEntry CreateSyntheticCompleteJob()
{
return new SourceEntry("Complete job", isSyntheticCompleteJob: true);
}
public IStep Step { get; set; }
public string DisplayName { get; }
public string MatchKey { get; }
public bool IsSyntheticCompleteJob { get; }
}
private enum SourceSection
{
Pre,
Main,
Post
}
}
}

View File

@@ -337,25 +337,7 @@ namespace GitHub.Runner.Worker
}
step.ExecutionContext = Root.CreatePostChild(step.DisplayName, IntraActionState, siblingScopeName);
if (step is JobExtensionRunner)
{
step.ExecutionContext.StepTelemetry.Type = "runner";
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(
@@ -988,8 +970,7 @@ namespace GitHub.Runner.Worker
Global.WriteDebug = Global.Variables.Step_Debug ?? false;
// Debugger enabled flag (from acquire response).
var overrideDebuggerWelcomeMessage = Global.Variables.GetBoolean(Constants.Runner.Features.OverrideDebuggerWelcomeMessage) ?? false;
Global.Debugger = new Dap.DebuggerConfig(message.EnableDebugger, message.DebuggerTunnel, overrideDebuggerWelcomeMessage, message.DebuggerWelcomeMessage);
Global.Debugger = new Dap.DebuggerConfig(message.EnableDebugger, message.DebuggerTunnel);
// Hook up JobServerQueueThrottling event, we will log warning on server tarpit.
_jobServerQueue.JobServerQueueThrottling += JobServerQueueThrottling_EventReceived;

View File

@@ -12,7 +12,6 @@ using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Container.ContainerHooks;
using GitHub.Services.Common;
namespace GitHub.Runner.Worker.Handlers
{
@@ -129,15 +128,6 @@ namespace GitHub.Runner.Worker.Handlers
// file name character on Linux.
string arguments = StepHost.ResolvePathForStepHost(ExecutionContext, StringUtil.Format(@"""{0}""", target.Replace(@"""", @"\""")));
// Disable maglev jit compiler in node.js 24.x.x on x64 Windows until the node.js bug is fixed.
// https://github.com/nodejs/node/issues/62260
if (nodeRuntimeVersion.StartsWith("node24", StringComparison.OrdinalIgnoreCase) &&
(StringUtil.ConvertToBoolean(System.Environment.GetEnvironmentVariable("ACTIONS_RUNNER_DISABLE_NODE_MAGLEV")) || StringUtil.ConvertToBoolean(Environment.GetValueOrDefault("ACTIONS_RUNNER_DISABLE_NODE_MAGLEV"))))
{
Trace.Info("Disable maglev jit compiler in node.js");
arguments = $"--no-maglev {arguments}";
}
#if OS_WINDOWS
// It appears that node.exe outputs UTF8 when not in TTY mode.
Encoding outputEncoding = Encoding.UTF8;

View File

@@ -13,7 +13,6 @@ 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;
@@ -231,12 +230,6 @@ 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)

View File

@@ -20,7 +20,7 @@
<ItemGroup>
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="10.0.3" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="10.0.3" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="10.0.8" />
<PackageReference Include="System.Threading.Channels" Version="10.0.3" />
<PackageReference Include="YamlDotNet.Signed" Version="5.3.0" />
<PackageReference Include="Microsoft.DevTunnels.Connections" Version="1.3.39" />

View File

@@ -267,21 +267,6 @@ namespace GitHub.DistributedTask.Pipelines
set;
}
/// <summary>
/// Optional welcome message shown in the debugger console when a client connects.
/// Only used when the <c>actions_runner_override_debugger_welcome_message</c>
/// feature flag is set to <c>true</c> in the job variables. With the flag set,
/// a non-empty value is shown as-is and a null or empty value suppresses the
/// default welcome message. When the flag is not set, the runner shows its
/// built-in help text and this field is ignored.
/// </summary>
[DataMember(EmitDefaultValue = false)]
public string DebuggerWelcomeMessage
{
get;
set;
}
/// <summary>
/// Gets the workflow-level action dependencies (lockfile entries)
/// </summary>

View File

@@ -186,16 +186,7 @@
"vars",
"needs",
"strategy",
"matrix",
"steps",
"job",
"runner",
"env",
"always(0,0)",
"failure(0,0)",
"cancelled(0,0)",
"success(0,0)",
"hashFiles(1,255)"
"matrix"
],
"string": {}
},

View File

@@ -2291,10 +2291,6 @@ namespace GitHub.Actions.WorkflowParser.Conversion
new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Needs),
new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Strategy),
new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Matrix),
new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Steps),
new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Job),
new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Runner),
new NamedValueInfo<NoOperationNamedValue>(WorkflowTemplateConstants.Env),
};
private static readonly IFunctionInfo[] s_jobConditionFunctions = new IFunctionInfo[]
{
@@ -2311,13 +2307,6 @@ namespace GitHub.Actions.WorkflowParser.Conversion
new FunctionInfo<NoOperation>(WorkflowTemplateConstants.Success, 0, 0),
new FunctionInfo<NoOperation>(WorkflowTemplateConstants.HashFiles, 1, Byte.MaxValue),
};
private static readonly IFunctionInfo[] s_snapshotConditionFunctions = new IFunctionInfo[]
{
new FunctionInfo<NoOperation>(WorkflowTemplateConstants.Always, 0, 0),
new FunctionInfo<NoOperation>(WorkflowTemplateConstants.Cancelled, 0, 0),
new FunctionInfo<NoOperation>(WorkflowTemplateConstants.Failure, 0, 0),
new FunctionInfo<NoOperation>(WorkflowTemplateConstants.Success, 0, 0),
new FunctionInfo<NoOperation>(WorkflowTemplateConstants.HashFiles, 1, Byte.MaxValue),
};
private static readonly IFunctionInfo[] s_snapshotConditionFunctions = null;
}
}

View File

@@ -2196,16 +2196,7 @@
"vars",
"needs",
"strategy",
"matrix",
"steps",
"job",
"runner",
"env",
"always(0,0)",
"failure(0,0)",
"cancelled(0,0)",
"success(0,0)",
"hashFiles(1,255)"
"matrix"
],
"description": "Use the if conditional to prevent a snapshot from being taken unless a condition is met. Any supported context and expression can be used to create a conditional. Expressions in an `if` conditional do not require the bracketed expression syntax. When you use expressions in an `if` conditional, you may omit the expression syntax because GitHub automatically evaluates the `if` conditional as an expression.",
"string": {

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Json;
using System.Text;
@@ -17,13 +17,13 @@ public sealed class AgentJobRequestMessageL0
// Arrange
var serializer = new DataContractJsonSerializer(typeof(AgentJobRequestMessage));
string jsonWithEnabledDebugger = DoubleQuotify("{'EnableDebugger': true}");
// Act
using var stream = new MemoryStream();
stream.Write(Encoding.UTF8.GetBytes(jsonWithEnabledDebugger));
stream.Position = 0;
var recoveredMessage = serializer.ReadObject(stream) as AgentJobRequestMessage;
// Assert
Assert.NotNull(recoveredMessage);
Assert.True(recoveredMessage.EnableDebugger, "EnableDebugger should be true when JSON contains 'EnableDebugger': true");
@@ -37,13 +37,13 @@ public sealed class AgentJobRequestMessageL0
// Arrange
var serializer = new DataContractJsonSerializer(typeof(AgentJobRequestMessage));
string jsonWithoutDebugger = DoubleQuotify("{'messageType': 'PipelineAgentJobRequest'}");
// Act
using var stream = new MemoryStream();
stream.Write(Encoding.UTF8.GetBytes(jsonWithoutDebugger));
stream.Position = 0;
var recoveredMessage = serializer.ReadObject(stream) as AgentJobRequestMessage;
// Assert
Assert.NotNull(recoveredMessage);
Assert.False(recoveredMessage.EnableDebugger, "EnableDebugger should default to false when JSON field is absent");
@@ -57,13 +57,13 @@ public sealed class AgentJobRequestMessageL0
// Arrange
var serializer = new DataContractJsonSerializer(typeof(AgentJobRequestMessage));
string jsonWithDisabledDebugger = DoubleQuotify("{'EnableDebugger': false}");
// Act
using var stream = new MemoryStream();
stream.Write(Encoding.UTF8.GetBytes(jsonWithDisabledDebugger));
stream.Position = 0;
var recoveredMessage = serializer.ReadObject(stream) as AgentJobRequestMessage;
// Assert
Assert.NotNull(recoveredMessage);
Assert.False(recoveredMessage.EnableDebugger, "EnableDebugger should be false when JSON contains 'EnableDebugger': false");
@@ -161,26 +161,6 @@ public sealed class AgentJobRequestMessageL0
Assert.Empty(recoveredMessage.ActionsDependencies);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void VerifyDebuggerWelcomeMessageRoundTrips()
{
// Arrange
var serializer = new DataContractJsonSerializer(typeof(AgentJobRequestMessage));
string json = DoubleQuotify("{'DebuggerWelcomeMessage': 'Welcome to debugging!'}");
// Act
using var stream = new MemoryStream();
stream.Write(Encoding.UTF8.GetBytes(json));
stream.Position = 0;
var recoveredMessage = serializer.ReadObject(stream) as AgentJobRequestMessage;
// Assert
Assert.NotNull(recoveredMessage);
Assert.Equal("Welcome to debugging!", recoveredMessage.DebuggerWelcomeMessage);
}
private static string DoubleQuotify(string text)
{
return text.Replace('\'', '"');

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
@@ -11,9 +11,7 @@ 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
{
@@ -238,7 +236,7 @@ namespace GitHub.Runner.Common.Tests.Worker
}
}
private static Mock<IExecutionContext> CreateJobContextWithTunnel(CancellationToken cancellationToken, ushort port, string jobName = null, bool overrideWelcomeMessage = false, string welcomeMessage = null)
private static Mock<IExecutionContext> CreateJobContextWithTunnel(CancellationToken cancellationToken, ushort port, string jobName = null)
{
var tunnel = new GitHub.DistributedTask.Pipelines.DebuggerTunnelInfo
{
@@ -247,7 +245,7 @@ namespace GitHub.Runner.Common.Tests.Worker
HostToken = "test-token",
Port = port
};
var debuggerConfig = new DebuggerConfig(true, tunnel, overrideWelcomeMessage, welcomeMessage);
var debuggerConfig = new DebuggerConfig(true, tunnel);
var jobContext = new Mock<IExecutionContext>();
jobContext.Setup(x => x.CancellationToken).Returns(cancellationToken);
jobContext.Setup(x => x.Global).Returns(new GlobalContext { Debugger = debuggerConfig });
@@ -257,78 +255,6 @@ 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")]
@@ -792,325 +718,6 @@ 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: \"Setup 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: \"Setup 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")]
@@ -1135,15 +742,8 @@ namespace GitHub.Runner.Common.Tests.Worker
// Read the configurationDone response
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
// Read the welcome message output event
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();
@@ -1152,26 +752,10 @@ namespace GitHub.Runner.Common.Tests.Worker
var stoppedMsg = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
Assert.Contains("\"event\":\"stopped\"", stoppedMsg);
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,
Seq = 2,
Type = "request",
Command = "continue"
});
@@ -1191,68 +775,6 @@ 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")]
@@ -1327,8 +849,6 @@ namespace GitHub.Runner.Common.Tests.Worker
Command = "configurationDone"
});
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
// Read the welcome message output event
await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
await waitTask;
@@ -1347,224 +867,5 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.Equal(completedTask, finished);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task WelcomeMessageSendsDefaultHelpWhenOverrideDisabled()
{
using (CreateTestContext())
{
var port = GetFreePort();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var jobContext = CreateJobContextWithTunnel(cts.Token, port);
await _debugger.StartAsync(jobContext.Object);
using var client = await ConnectClientAsync(port);
var stream = client.GetStream();
await SendRequestAsync(stream, new Request
{
Seq = 1,
Type = "request",
Command = "configurationDone"
});
// First message: configurationDone response
var configDoneResponse = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
Assert.Contains("\"command\":\"configurationDone\"", configDoneResponse);
// Second message: welcome output event with default help text
var welcomeMsg = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
Assert.Contains("\"event\":\"output\"", welcomeMsg);
Assert.Contains("\"category\":\"console\"", welcomeMsg);
Assert.Contains("Actions Debug Console", welcomeMsg);
Assert.Contains("help", welcomeMsg);
await _debugger.StopAsync();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task WelcomeMessageShowsCustomMessageWhenOverrideEnabled()
{
using (CreateTestContext())
{
var port = GetFreePort();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var jobContext = CreateJobContextWithTunnel(cts.Token, port,
overrideWelcomeMessage: true,
welcomeMessage: "Welcome to debugging!");
await _debugger.StartAsync(jobContext.Object);
using var client = await ConnectClientAsync(port);
var stream = client.GetStream();
await SendRequestAsync(stream, new Request
{
Seq = 1,
Type = "request",
Command = "configurationDone"
});
// First: configurationDone response
var configDoneResponse = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
Assert.Contains("\"command\":\"configurationDone\"", configDoneResponse);
// Second: custom welcome message
var welcomeMsg = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
Assert.Contains("\"event\":\"output\"", welcomeMsg);
Assert.Contains("Welcome to debugging!", welcomeMsg);
await _debugger.StopAsync();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task WelcomeMessageSuppressedWhenOverrideEnabledWithEmptyMessage()
{
using (CreateTestContext())
{
var port = GetFreePort();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var jobContext = CreateJobContextWithTunnel(cts.Token, port,
overrideWelcomeMessage: true,
welcomeMessage: "");
await _debugger.StartAsync(jobContext.Object);
using var client = await ConnectClientAsync(port);
var stream = client.GetStream();
await SendRequestAsync(stream, new Request
{
Seq = 1,
Type = "request",
Command = "configurationDone"
});
// Read configurationDone response
var configDoneResponse = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
Assert.Contains("\"command\":\"configurationDone\"", configDoneResponse);
// Send threads request — if welcome message was suppressed, this
// should be the next response (no output event in between)
await SendRequestAsync(stream, new Request
{
Seq = 2,
Type = "request",
Command = "threads"
});
var threadsResponse = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
Assert.Contains("\"command\":\"threads\"", threadsResponse);
await _debugger.StopAsync();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task WelcomeMessageSuppressedWhenOverrideEnabledWithNullMessage()
{
using (CreateTestContext())
{
var port = GetFreePort();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var jobContext = CreateJobContextWithTunnel(cts.Token, port,
overrideWelcomeMessage: true,
welcomeMessage: null);
await _debugger.StartAsync(jobContext.Object);
using var client = await ConnectClientAsync(port);
var stream = client.GetStream();
await SendRequestAsync(stream, new Request
{
Seq = 1,
Type = "request",
Command = "configurationDone"
});
// Read configurationDone response
var configDoneResponse = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
Assert.Contains("\"command\":\"configurationDone\"", configDoneResponse);
// Send threads request — if welcome message was suppressed, this
// should be the next response (no output event in between)
await SendRequestAsync(stream, new Request
{
Seq = 2,
Type = "request",
Command = "threads"
});
var threadsResponse = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
Assert.Contains("\"command\":\"threads\"", threadsResponse);
await _debugger.StopAsync();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public async Task WelcomeMessageSentOnlyOnce()
{
using (CreateTestContext())
{
var port = GetFreePort();
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
var jobContext = CreateJobContextWithTunnel(cts.Token, port);
await _debugger.StartAsync(jobContext.Object);
using var client = await ConnectClientAsync(port);
var stream = client.GetStream();
// First configurationDone
await SendRequestAsync(stream, new Request
{
Seq = 1,
Type = "request",
Command = "configurationDone"
});
var configDoneResponse = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
Assert.Contains("\"command\":\"configurationDone\"", configDoneResponse);
// Welcome message should appear
var welcomeMsg = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
Assert.Contains("\"event\":\"output\"", welcomeMsg);
Assert.Contains("Actions Debug Console", welcomeMsg);
// Second configurationDone — should NOT produce another welcome message
await SendRequestAsync(stream, new Request
{
Seq = 2,
Type = "request",
Command = "configurationDone"
});
var secondResponse = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
Assert.Contains("\"command\":\"configurationDone\"", secondResponse);
// Next message should be threads response, not another welcome output
await SendRequestAsync(stream, new Request
{
Seq = 3,
Type = "request",
Command = "threads"
});
var threadsResponse = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5));
Assert.Contains("\"command\":\"threads\"", threadsResponse);
await _debugger.StopAsync();
}
}
}
}

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Xunit;
@@ -171,36 +171,6 @@ 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")]

View File

@@ -5,12 +5,9 @@ using System.Threading;
using System.Threading.Tasks;
using GitHub.DistributedTask.Expressions2;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common.Tests;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Dap;
using GitHub.Runner.Worker.Handlers;
using Moq;
using Xunit;
@@ -43,8 +40,7 @@ namespace GitHub.Runner.Common.Tests.Worker
private Mock<IExecutionContext> CreateMockContext(
DictionaryContextData exprValues = null,
IDictionary<string, IDictionary<string, string>> jobDefaults = null,
ContainerInfo container = null)
IDictionary<string, IDictionary<string, string>> jobDefaults = null)
{
var mock = new Mock<IExecutionContext>();
mock.Setup(x => x.ExpressionValues).Returns(exprValues ?? new DictionaryContextData());
@@ -55,7 +51,6 @@ namespace GitHub.Runner.Common.Tests.Worker
PrependPath = new List<string>(),
JobDefaults = jobDefaults
?? new Dictionary<string, IDictionary<string, string>>(StringComparer.OrdinalIgnoreCase),
Container = container,
};
mock.Setup(x => x.Global).Returns(global);
@@ -70,7 +65,7 @@ namespace GitHub.Runner.Common.Tests.Worker
using (CreateTestContext())
{
var command = new RunCommand { Script = "echo hello" };
var result = await _executor.ExecuteRunCommandAsync(command, null, false, CancellationToken.None);
var result = await _executor.ExecuteRunCommandAsync(command, null, CancellationToken.None);
Assert.Equal("error", result.Type);
Assert.Contains("No execution context available", result.Result);
@@ -238,101 +233,5 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.False(result.ContainsKey("BAZ"));
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CreateStepHost_NoContainer_ReturnsDefaultStepHost()
{
using (var hc = CreateTestContext())
{
hc.EnqueueInstance<IDefaultStepHost>(new DefaultStepHost());
var context = CreateMockContext();
var result = _executor.CreateStepHost(context.Object, isActionStep: true);
Assert.IsType<DefaultStepHost>(result);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CreateStepHost_WithContainer_ActionStep_ReturnsContainerStepHost()
{
using (var hc = CreateTestContext())
{
hc.EnqueueInstance<IContainerStepHost>(new ContainerStepHost());
var container = new ContainerInfo { ContainerId = "abc123" };
var context = CreateMockContext(container: container);
var result = _executor.CreateStepHost(context.Object, isActionStep: true);
Assert.IsType<ContainerStepHost>(result);
var containerHost = (ContainerStepHost)result;
Assert.Same(container, containerHost.Container);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CreateStepHost_WithContainer_InfrastructureStep_ReturnsDefaultStepHost()
{
using (var hc = CreateTestContext())
{
hc.EnqueueInstance<IDefaultStepHost>(new DefaultStepHost());
var container = new ContainerInfo { ContainerId = "abc123" };
var context = CreateMockContext(container: container);
var result = _executor.CreateStepHost(context.Object, isActionStep: false);
Assert.IsType<DefaultStepHost>(result);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CreateStepHost_ContainerWithoutId_NoHooks_ReturnsDefaultStepHost()
{
using (var hc = CreateTestContext())
{
hc.EnqueueInstance<IDefaultStepHost>(new DefaultStepHost());
// Container exists but hasn't been started yet (no ContainerId)
var container = new ContainerInfo();
var context = CreateMockContext(container: container);
var result = _executor.CreateStepHost(context.Object, isActionStep: true);
Assert.IsType<DefaultStepHost>(result);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CreateStepHost_ContainerWithoutId_HooksEnabled_ReturnsContainerStepHost()
{
using (var hc = CreateTestContext())
{
hc.EnqueueInstance<IContainerStepHost>(new ContainerStepHost());
// Container hooks need both the feature flag and the env var
Environment.SetEnvironmentVariable("ACTIONS_RUNNER_CONTAINER_HOOKS", "/some/hook/path");
try
{
var container = new ContainerInfo();
var context = CreateMockContext(container: container);
context.Object.Global.Variables = new Variables(
hc,
new Dictionary<string, VariableValue>
{
{ Constants.Runner.Features.AllowRunnerContainerHooks, new VariableValue("true") }
});
var result = _executor.CreateStepHost(context.Object, isActionStep: true);
Assert.IsAssignableFrom<IContainerStepHost>(result);
}
finally
{
Environment.SetEnvironmentVariable("ACTIONS_RUNNER_CONTAINER_HOOKS", null);
}
}
}
}
}

View File

@@ -361,119 +361,6 @@ namespace GitHub.Runner.Common.Tests.Worker
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void RegisterPostJobStep_JobExtensionRunner_DefaultsRunnerTelemetry()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange: Create a job request message.
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 pagingLogger1 = new Mock<IPagingLogger>();
var pagingLogger2 = new Mock<IPagingLogger>();
var jobServerQueue = new Mock<IJobServerQueue>();
jobServerQueue.Setup(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.IsAny<TimelineRecord>()));
hc.EnqueueInstance(pagingLogger1.Object);
hc.EnqueueInstance(pagingLogger2.Object);
hc.SetSingleton(jobServerQueue.Object);
var jobContext = new Runner.Worker.ExecutionContext();
jobContext.Initialize(hc);
// Act.
jobContext.InitializeJob(jobRequest, CancellationToken.None);
var extensionStep = new JobExtensionRunner(
runAsync: (_, _) => System.Threading.Tasks.Task.CompletedTask,
condition: "always()",
displayName: "Create Custom Image",
data: null);
jobContext.RegisterPostJobStep(extensionStep);
// Assert: telemetry defaults are populated for non-action post-job steps.
Assert.NotNull(extensionStep.ExecutionContext);
Assert.Equal("runner", extensionStep.ExecutionContext.StepTelemetry.Type);
Assert.Equal("create_custom_image", extensionStep.ExecutionContext.StepTelemetry.Action);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void RegisterPostJobStep_ActionRunner_DoesNotOverrideTelemetry()
{
using (TestHostContext hc = CreateTestContext())
{
// Arrange: Create a job request message.
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 pagingLogger1 = new Mock<IPagingLogger>();
var pagingLogger2 = new Mock<IPagingLogger>();
var pagingLogger3 = new Mock<IPagingLogger>();
var pagingLogger4 = new Mock<IPagingLogger>();
var jobServerQueue = new Mock<IJobServerQueue>();
jobServerQueue.Setup(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.IsAny<TimelineRecord>()));
var actionRunner = new ActionRunner();
actionRunner.Initialize(hc);
hc.EnqueueInstance(pagingLogger1.Object);
hc.EnqueueInstance(pagingLogger2.Object);
hc.EnqueueInstance(pagingLogger3.Object);
hc.EnqueueInstance(pagingLogger4.Object);
hc.EnqueueInstance(actionRunner as IActionRunner);
hc.SetSingleton(jobServerQueue.Object);
var jobContext = new Runner.Worker.ExecutionContext();
jobContext.Initialize(hc);
// Act.
jobContext.InitializeJob(jobRequest, CancellationToken.None);
var action = jobContext.CreateChild(Guid.NewGuid(), "action", "action", null, null, 0);
var postRunner = hc.CreateService<IActionRunner>();
postRunner.Action = new Pipelines.ActionStep() { Id = Guid.NewGuid(), Name = "post", DisplayName = "Post Action", Reference = new Pipelines.RepositoryPathReference() { Name = "actions/action" } };
postRunner.Stage = ActionRunStage.Post;
postRunner.Condition = "always()";
postRunner.DisplayName = "Post Action";
action.RegisterPostJobStep(postRunner);
// Assert: action post-step telemetry is left for the handler to fill in,
// so RegisterPostJobStep should NOT pre-populate runner-owned defaults.
Assert.NotNull(postRunner.ExecutionContext);
Assert.Null(postRunner.ExecutionContext.StepTelemetry.Type);
Assert.Null(postRunner.ExecutionContext.StepTelemetry.Action);
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]