Compare commits

..

4 Commits

Author SHA1 Message Date
Salman Chishti
ed0e5b75ee Add regression tests for path casing normalization
PathUtilL0:
- Folder casing normalization (create MiXeDcAsE, query lowercase)
- Idempotency (calling twice returns same result)
- Input casing independence (upper and lower resolve to same canonical)

HostContextL0:
- Root directory returns cached value across calls
- Derived paths (Diag, Externals) share Root prefix casing
2026-05-06 22:37:41 +01:00
Salman Chishti
fffded93ac Cache canonical root path to avoid repeated API calls
GetDirectory(WellKnownDirectory.Root) is called ~44 times during a run.
Cache the result since the root directory is immutable for the lifetime
of HostContext.
2026-05-06 22:37:04 +01:00
Salman Chishti
8307b8fe33 Handle long paths, UNC paths, and UNC temp in tests
Retry GetFinalPathNameByHandle with a larger buffer when the path
exceeds 1024 chars. Handle \?\UNC\ prefix conversion to standard
UNC paths. Use StringComparison.Ordinal for prefix checks. Skip the
drive letter test when TEMP is a UNC path.
2026-05-06 22:36:30 +01:00
Salman Chishti
7585eb30aa Normalize Windows path casing using GetFinalPathNameByHandle
On Windows, the runner inherits whatever path casing is used to start it
(e.g. c:\actions-runner vs C:\actions-runner). NTFS is case-insensitive
but tools like git's includeIf.gitdir do exact string matching, causing
auth failures when the casing doesn't match the canonical NTFS path.

This adds PathUtil.GetCanonicalPath which uses the Win32
GetFinalPathNameByHandle API to resolve paths to their NTFS canonical
casing. It is called when resolving the runner root directory, so all
derived paths (workspace, temp, etc.) use the correct casing.

Fixes actions/checkout#2345
2026-05-06 22:28:30 +01:00
31 changed files with 379 additions and 1980 deletions

View File

@@ -4,7 +4,7 @@
"features": {
"ghcr.io/devcontainers/features/docker-in-docker:2": {},
"ghcr.io/devcontainers/features/dotnet": {
"version": "8.0.421"
"version": "8.0.420"
},
"ghcr.io/devcontainers/features/node:1": {
"version": "20"

3
.gitignore vendored
View File

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

View File

@@ -25,11 +25,11 @@ The `installdependencies.sh` script should install all required dependencies on
Debian based OS (Debian, Ubuntu, Linux Mint)
- liblttng-ust1t64, liblttng-ust1 or liblttng-ust0
- liblttng-ust1 or liblttng-ust0
- libkrb5-3
- zlib1g
- libssl3t64, libssl3, libssl1.1, libssl1.0.2 or libssl1.0.0
- libicu80, libicu79, ..., libicu66, libicu65, libicu63, libicu60, libicu57, libicu55, or libicu52
- libicu76, libicu75, ..., libicu66, libicu65, libicu63, libicu60, libicu57, libicu55, or libicu52
Fedora based OS (Fedora, Red Hat Enterprise Linux, CentOS, Oracle Linux 7)

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

@@ -94,7 +94,7 @@ then
fi
}
apt_get_with_fallbacks liblttng-ust1t64 liblttng-ust1 liblttng-ust0
apt_get_with_fallbacks liblttng-ust1 liblttng-ust0
if [ $? -ne 0 ]
then
echo "'$apt_get' failed with exit code '$?'"
@@ -110,7 +110,7 @@ then
exit 1
fi
apt_get_with_fallbacks libicu80 libicu79 libicu78 libicu77 libicu76 libicu75 libicu74 libicu73 libicu72 libicu71 libicu70 libicu69 libicu68 libicu67 libicu66 libicu65 libicu63 libicu60 libicu57 libicu55 libicu52
apt_get_with_fallbacks libicu76 libicu75 libicu74 libicu73 libicu72 libicu71 libicu70 libicu69 libicu68 libicu67 libicu66 libicu65 libicu63 libicu60 libicu57 libicu55 libicu52
if [ $? -ne 0 ]
then
echo "'$apt_get' failed with exit code '$?'"

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

@@ -64,6 +64,7 @@ namespace GitHub.Runner.Common
private readonly List<ProductInfoHeaderValue> _userAgents = new() { new ProductInfoHeaderValue($"GitHubActionsRunner-{BuildConstants.RunnerPackage.PackageName}", BuildConstants.RunnerPackage.Version) };
private CancellationTokenSource _runnerShutdownTokenSource = new();
private object _perfLock = new();
private string _canonicalRootDirectory;
private Tracing _trace;
private Tracing _actionsHttpTrace;
private Tracing _netcoreHttpTrace;
@@ -391,7 +392,12 @@ namespace GitHub.Runner.Common
break;
case WellKnownDirectory.Root:
path = new DirectoryInfo(GetDirectory(WellKnownDirectory.Bin)).Parent.FullName;
if (_canonicalRootDirectory == null)
{
_canonicalRootDirectory = PathUtil.GetCanonicalPath(
new DirectoryInfo(GetDirectory(WellKnownDirectory.Bin)).Parent.FullName);
}
path = _canonicalRootDirectory;
break;
case WellKnownDirectory.Temp:

View File

@@ -1,4 +1,7 @@
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.Win32.SafeHandles;
namespace GitHub.Runner.Sdk
{
@@ -6,8 +9,98 @@ namespace GitHub.Runner.Sdk
{
#if OS_WINDOWS
public static readonly string PathVariable = "Path";
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern SafeFileHandle CreateFile(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
System.IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
System.IntPtr hTemplateFile);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern uint GetFinalPathNameByHandle(
SafeFileHandle hFile,
[Out] StringBuilder lpszFilePath,
uint cchFilePath,
uint dwFlags);
private const uint FILE_READ_ATTRIBUTES = 0x80;
private const uint FILE_SHARE_READ = 0x1;
private const uint FILE_SHARE_WRITE = 0x2;
private const uint FILE_SHARE_DELETE = 0x4;
private const uint OPEN_EXISTING = 3;
private const uint FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
private const uint VOLUME_NAME_DOS = 0x0;
/// <summary>
/// Returns the NTFS canonical path for a directory, resolving drive letter
/// and folder name casing to match what is stored on disk.
/// On non-Windows platforms, returns the path unchanged.
/// </summary>
public static string GetCanonicalPath(string path)
{
if (string.IsNullOrEmpty(path) || !Directory.Exists(path))
{
return path;
}
using var handle = CreateFile(
path,
FILE_READ_ATTRIBUTES,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
System.IntPtr.Zero,
OPEN_EXISTING,
FILE_FLAG_BACKUP_SEMANTICS,
System.IntPtr.Zero);
if (handle.IsInvalid)
{
return path;
}
var buffer = new StringBuilder(1024);
var result = GetFinalPathNameByHandle(handle, buffer, (uint)buffer.Capacity, VOLUME_NAME_DOS);
if (result == 0)
{
return path;
}
// Retry with a larger buffer if the path was longer than expected
if (result >= buffer.Capacity)
{
buffer = new StringBuilder((int)result + 1);
result = GetFinalPathNameByHandle(handle, buffer, (uint)buffer.Capacity, VOLUME_NAME_DOS);
if (result == 0 || result >= buffer.Capacity)
{
return path;
}
}
var canonicalPath = buffer.ToString();
// Strip the \\?\UNC\ prefix and convert to standard UNC path
if (canonicalPath.StartsWith(@"\\?\UNC\", System.StringComparison.Ordinal))
{
canonicalPath = @"\\" + canonicalPath.Substring(8);
}
// Strip the \\?\ prefix for local paths
else if (canonicalPath.StartsWith(@"\\?\", System.StringComparison.Ordinal))
{
canonicalPath = canonicalPath.Substring(4);
}
return canonicalPath;
}
#else
public static readonly string PathVariable = "PATH";
public static string GetCanonicalPath(string path)
{
return path;
}
#endif
public static string PrependPath(string path, string currentPath)

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

@@ -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

@@ -299,6 +299,52 @@ namespace GitHub.Runner.Common.Tests
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetDirectoryRootReturnsCachedValue()
{
try
{
Setup();
// Call GetDirectory(Root) twice — should return the same reference
var root1 = _hc.GetDirectory(WellKnownDirectory.Root);
var root2 = _hc.GetDirectory(WellKnownDirectory.Root);
Assert.NotNull(root1);
Assert.Equal(root1, root2);
Assert.True(Directory.Exists(root1));
}
finally
{
Teardown();
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetDirectoryDerivedPathsUseRootCasing()
{
try
{
Setup();
var root = _hc.GetDirectory(WellKnownDirectory.Root);
var diag = _hc.GetDirectory(WellKnownDirectory.Diag);
var externals = _hc.GetDirectory(WellKnownDirectory.Externals);
// Diag and Externals should start with the same Root prefix
Assert.StartsWith(root, diag);
Assert.StartsWith(root, externals);
}
finally
{
Teardown();
}
}
private void Setup([CallerMemberName] string testName = "")
{
_tokenSource = new CancellationTokenSource();

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

@@ -0,0 +1,151 @@
using GitHub.Runner.Sdk;
using System.IO;
using System.Runtime.InteropServices;
using Xunit;
namespace GitHub.Runner.Common.Tests.Util
{
public sealed class PathUtilL0
{
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetCanonicalPath_ReturnsPath_WhenDirectoryDoesNotExist()
{
var fakePath = Path.Combine(Path.GetTempPath(), "nonexistent_" + Path.GetRandomFileName());
var result = PathUtil.GetCanonicalPath(fakePath);
Assert.Equal(fakePath, result);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetCanonicalPath_ReturnsPath_WhenNull()
{
Assert.Null(PathUtil.GetCanonicalPath(null));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetCanonicalPath_ReturnsEmpty_WhenEmpty()
{
Assert.Equal(string.Empty, PathUtil.GetCanonicalPath(string.Empty));
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetCanonicalPath_ReturnsValidPath_ForExistingDirectory()
{
var tempDir = Path.Combine(Path.GetTempPath(), "pathutil_test_" + Path.GetRandomFileName());
try
{
Directory.CreateDirectory(tempDir);
var result = PathUtil.GetCanonicalPath(tempDir);
Assert.NotNull(result);
Assert.True(Directory.Exists(result));
}
finally
{
if (Directory.Exists(tempDir))
{
Directory.Delete(tempDir);
}
}
}
#if OS_WINDOWS
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetCanonicalPath_NormalizesDriveLetter_OnWindows()
{
var tempDir = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar);
// Skip if temp is a UNC path (no drive letter to normalize)
if (tempDir.StartsWith(@"\\"))
{
return;
}
// Force lowercase drive letter
var lowerCased = char.ToLower(tempDir[0]) + tempDir.Substring(1);
var result = PathUtil.GetCanonicalPath(lowerCased);
// The canonical path should have an uppercase drive letter
Assert.True(char.IsUpper(result[0]),
$"Expected uppercase drive letter but got: {result}");
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetCanonicalPath_NormalizesFolderCasing_OnWindows()
{
// Create a directory with known casing, then query with wrong casing
var basePath = Path.GetTempPath();
if (basePath.StartsWith(@"\\"))
{
return; // Skip UNC
}
var realName = "PathUtilTest_MiXeDcAsE_" + Path.GetRandomFileName();
var realDir = Path.Combine(basePath, realName);
try
{
Directory.CreateDirectory(realDir);
// Query with all-lowercase version
var wrongCased = Path.Combine(basePath, realName.ToLowerInvariant());
var result = PathUtil.GetCanonicalPath(wrongCased);
// The canonical result should contain the original mixed-case name
Assert.Contains(realName, result);
}
finally
{
if (Directory.Exists(realDir))
{
Directory.Delete(realDir);
}
}
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetCanonicalPath_IsIdempotent_OnWindows()
{
// Calling GetCanonicalPath twice should return the same result
var tempDir = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar);
var first = PathUtil.GetCanonicalPath(tempDir);
var second = PathUtil.GetCanonicalPath(first);
Assert.Equal(first, second);
}
[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Common")]
public void GetCanonicalPath_ReturnsSameResult_RegardlessOfInputCasing_OnWindows()
{
var tempDir = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar);
if (tempDir.StartsWith(@"\\"))
{
return; // Skip UNC
}
var upper = tempDir.ToUpperInvariant();
var lower = tempDir.ToLowerInvariant();
var resultUpper = PathUtil.GetCanonicalPath(upper);
var resultLower = PathUtil.GetCanonicalPath(lower);
// Both should resolve to the same canonical path
Assert.Equal(resultUpper, resultLower);
}
#endif
}
}

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")]

View File

@@ -17,7 +17,7 @@ LAYOUT_DIR="$SCRIPT_DIR/../_layout"
DOWNLOAD_DIR="$SCRIPT_DIR/../_downloads/netcore2x"
PACKAGE_DIR="$SCRIPT_DIR/../_package"
DOTNETSDK_ROOT="$SCRIPT_DIR/../_dotnetsdk"
DOTNETSDK_VERSION="8.0.421"
DOTNETSDK_VERSION="8.0.420"
DOTNETSDK_INSTALLDIR="$DOTNETSDK_ROOT/$DOTNETSDK_VERSION"
RUNNER_VERSION=$(cat runnerversion)

View File

@@ -1,5 +1,5 @@
{
"sdk": {
"version": "8.0.421"
"version": "8.0.420"
}
}