From 4259ffb6dcd98827187261ee9accc3b7fde12516 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:02:30 +0000 Subject: [PATCH 01/15] Bump flatted from 3.2.7 to 3.4.2 in /src/Misc/expressionFunc/hashFiles (#4307) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/Misc/expressionFunc/hashFiles/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Misc/expressionFunc/hashFiles/package-lock.json b/src/Misc/expressionFunc/hashFiles/package-lock.json index 921a9b7c8..bfabff2f0 100644 --- a/src/Misc/expressionFunc/hashFiles/package-lock.json +++ b/src/Misc/expressionFunc/hashFiles/package-lock.json @@ -2187,9 +2187,9 @@ } }, "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true }, "node_modules/for-each": { @@ -6198,9 +6198,9 @@ } }, "flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true }, "for-each": { From e17e7aabbf56b2bf486ab46bad3cae57d000ace3 Mon Sep 17 00:00:00 2001 From: Francesco Renzi Date: Mon, 23 Mar 2026 14:02:15 +0000 Subject: [PATCH 02/15] Add DAP server (#4298) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Tingluo Huang --- src/Runner.Worker/Dap/DapDebugger.cs | 1299 +++++++++++++++++ src/Runner.Worker/Dap/DapMessages.cs | 1231 ++++++++++++++++ src/Runner.Worker/Dap/DapReplExecutor.cs | 369 +++++ src/Runner.Worker/Dap/DapReplParser.cs | 411 ++++++ src/Runner.Worker/Dap/DapVariableProvider.cs | 373 +++++ src/Runner.Worker/Dap/IDapDebugger.cs | 26 + src/Runner.Worker/ExecutionContext.cs | 3 + src/Runner.Worker/GlobalContext.cs | 1 + src/Runner.Worker/InternalsVisibleTo.cs | 1 + src/Runner.Worker/JobRunner.cs | 64 + src/Runner.Worker/StepsRunner.cs | 8 + .../Pipelines/AgentJobRequestMessage.cs | 7 + .../Sdk/RSWebApi/AgentJobRequestMessageL0.cs | 76 + src/Test/L0/ServiceInterfacesL0.cs | 1 + src/Test/L0/Worker/DapDebuggerL0.cs | 616 ++++++++ src/Test/L0/Worker/DapMessagesL0.cs | 233 +++ src/Test/L0/Worker/DapReplExecutorL0.cs | 237 +++ src/Test/L0/Worker/DapReplParserL0.cs | 314 ++++ src/Test/L0/Worker/DapVariableProviderL0.cs | 728 +++++++++ src/Test/L0/Worker/JobExtensionL0.cs | 5 + src/Test/L0/Worker/StepsRunnerL0.cs | 5 + 21 files changed, 6008 insertions(+) create mode 100644 src/Runner.Worker/Dap/DapDebugger.cs create mode 100644 src/Runner.Worker/Dap/DapMessages.cs create mode 100644 src/Runner.Worker/Dap/DapReplExecutor.cs create mode 100644 src/Runner.Worker/Dap/DapReplParser.cs create mode 100644 src/Runner.Worker/Dap/DapVariableProvider.cs create mode 100644 src/Runner.Worker/Dap/IDapDebugger.cs create mode 100644 src/Test/L0/Sdk/RSWebApi/AgentJobRequestMessageL0.cs create mode 100644 src/Test/L0/Worker/DapDebuggerL0.cs create mode 100644 src/Test/L0/Worker/DapMessagesL0.cs create mode 100644 src/Test/L0/Worker/DapReplExecutorL0.cs create mode 100644 src/Test/L0/Worker/DapReplParserL0.cs create mode 100644 src/Test/L0/Worker/DapVariableProviderL0.cs diff --git a/src/Runner.Worker/Dap/DapDebugger.cs b/src/Runner.Worker/Dap/DapDebugger.cs new file mode 100644 index 000000000..9d0acca68 --- /dev/null +++ b/src/Runner.Worker/Dap/DapDebugger.cs @@ -0,0 +1,1299 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using GitHub.DistributedTask.WebApi; +using GitHub.Runner.Common; +using GitHub.Runner.Sdk; +using Newtonsoft.Json; + +namespace GitHub.Runner.Worker.Dap +{ + /// + /// Stores information about a completed step for stack trace display. + /// + internal sealed class CompletedStepInfo + { + public string DisplayName { get; set; } + public TaskResult? Result { get; set; } + public int FrameId { get; set; } + } + + /// + /// Single public facade for the Debug Adapter Protocol subsystem. + /// Owns the full transport, handshake, step-level pauses, variable + /// inspection, reconnection, and cancellation flow. + /// + public sealed class DapDebugger : RunnerService, IDapDebugger + { + private const int _defaultPort = 4711; + private const int _defaultTimeoutMinutes = 15; + private const string _portEnvironmentVariable = "ACTIONS_RUNNER_DAP_PORT"; + private const string _timeoutEnvironmentVariable = "ACTIONS_RUNNER_DAP_CONNECTION_TIMEOUT"; + private const string _contentLengthHeader = "Content-Length: "; + private const int _maxMessageSize = 10 * 1024 * 1024; // 10 MB + private const int _maxHeaderLineLength = 8192; // 8 KB + private const int _connectionRetryDelayMilliseconds = 500; + + // Thread ID for the single job execution thread + private const int _jobThreadId = 1; + + // Frame ID for the current step (always 1) + private const int _currentFrameId = 1; + + // Frame IDs for completed steps start at 1000 + private const int _completedFrameIdBase = 1000; + + private TcpListener _listener; + private TcpClient _client; + private NetworkStream _stream; + private readonly SemaphoreSlim _sendLock = new SemaphoreSlim(1, 1); + private int _nextSeq = 1; + private Task _connectionLoopTask; + private volatile DapSessionState _state = DapSessionState.NotStarted; + private CancellationTokenRegistration? _cancellationRegistration; + private bool _isFirstStep = true; + + // Synchronization for step execution + private TaskCompletionSource _commandTcs; + private readonly object _stateLock = new object(); + + // Session readiness — signaled when configurationDone is received + private TaskCompletionSource _readyTcs; + + // Whether to pause before the next step (set by 'next' command) + private bool _pauseOnNextStep = true; + + // Current execution context + private IStep _currentStep; + private IExecutionContext _jobContext; + private int _currentStepIndex; + + // Track completed steps for stack trace + private readonly List _completedSteps = new List(); + private int _nextCompletedFrameId = _completedFrameIdBase; + + // Client connection tracking for reconnection support + private volatile bool _isClientConnected; + + // Scope/variable inspection provider — reusable by future DAP features + private DapVariableProvider _variableProvider; + + // REPL command executor for run() commands + private DapReplExecutor _replExecutor; + + public bool IsActive => + _state == DapSessionState.Ready || + _state == DapSessionState.Paused || + _state == DapSessionState.Running; + + internal DapSessionState State => _state; + + public override void Initialize(IHostContext hostContext) + { + base.Initialize(hostContext); + _variableProvider = new DapVariableProvider(hostContext.SecretMasker); + _replExecutor = new DapReplExecutor(hostContext, SendOutput); + Trace.Info("DapDebugger initialized"); + } + + public Task StartAsync(IExecutionContext jobContext) + { + ArgUtil.NotNull(jobContext, nameof(jobContext)); + var port = ResolvePort(); + + Trace.Info($"Starting DAP debugger on port {port}"); + + _jobContext = jobContext; + _readyTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + _listener = new TcpListener(IPAddress.Loopback, port); + _listener.Start(); + Trace.Info($"DAP debugger listening on {_listener.LocalEndpoint}"); + + _state = DapSessionState.WaitingForConnection; + _connectionLoopTask = ConnectionLoopAsync(jobContext.CancellationToken); + + _cancellationRegistration = jobContext.CancellationToken.Register(() => + { + Trace.Info("Job cancellation requested, unblocking pending waits."); + _readyTcs?.TrySetCanceled(); + _commandTcs?.TrySetResult(DapCommand.Disconnect); + }); + + Trace.Info($"DAP debugger started on port {port}"); + return Task.CompletedTask; + } + + public async Task WaitUntilReadyAsync() + { + if (_state == DapSessionState.NotStarted || _listener == null || _jobContext == null) + { + return; + } + + var timeoutMinutes = ResolveTimeout(); + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMinutes(timeoutMinutes)); + + try + { + Trace.Info($"Waiting for debugger client connection (timeout: {timeoutMinutes} minutes)..."); + using (timeoutCts.Token.Register(() => _readyTcs?.TrySetCanceled())) + { + await _readyTcs.Task; + } + + Trace.Info("DAP debugger ready."); + } + catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested && !_jobContext.CancellationToken.IsCancellationRequested) + { + throw new TimeoutException($"No debugger client connected within {timeoutMinutes} minutes."); + } + } + + public async Task OnJobCompletedAsync() + { + if (_state != DapSessionState.NotStarted) + { + try + { + OnJobCompleted(); + } + catch (Exception ex) + { + Trace.Warning($"DAP OnJobCompleted error: {ex.Message}"); + } + } + + await StopAsync(); + } + + public async Task StopAsync() + { + if (_cancellationRegistration.HasValue) + { + _cancellationRegistration.Value.Dispose(); + _cancellationRegistration = null; + } + + if (_state != DapSessionState.NotStarted) + { + try + { + Trace.Info("Stopping DAP debugger"); + + CleanupConnection(); + + try { _listener?.Stop(); } + catch { /* best effort */ } + + if (_connectionLoopTask != null) + { + try + { + await Task.WhenAny(_connectionLoopTask, Task.Delay(5000)); + } + catch { /* best effort */ } + } + } + catch (Exception ex) + { + Trace.Error("Error stopping DAP debugger"); + Trace.Error(ex); + } + } + + lock (_stateLock) + { + if (_state != DapSessionState.NotStarted && _state != DapSessionState.Terminated) + { + _state = DapSessionState.Terminated; + } + } + + _isClientConnected = false; + _listener = null; + _client = null; + _stream = null; + _readyTcs = null; + _connectionLoopTask = null; + } + + public async Task OnStepStartingAsync(IStep step) + { + if (!IsActive) + { + return; + } + + try + { + bool isFirst = _isFirstStep; + _isFirstStep = false; + await OnStepStartingAsync(step, isFirst); + } + catch (Exception ex) + { + Trace.Warning($"DAP OnStepStarting error: {ex.Message}"); + } + } + + public void OnStepCompleted(IStep step) + { + if (!IsActive) + { + return; + } + + try + { + var result = step.ExecutionContext?.Result; + Trace.Info("Step completed"); + + // Add to completed steps list for stack trace + lock (_stateLock) + { + if (_state != DapSessionState.Ready && + _state != DapSessionState.Paused && + _state != DapSessionState.Running) + { + return; + } + + _completedSteps.Add(new CompletedStepInfo + { + DisplayName = step.DisplayName, + Result = result, + FrameId = _nextCompletedFrameId++ + }); + } + } + catch (Exception ex) + { + Trace.Warning($"DAP OnStepCompleted error: {ex.Message}"); + } + } + + internal async Task HandleMessageAsync(string messageJson, CancellationToken cancellationToken) + { + Request request = null; + try + { + request = JsonConvert.DeserializeObject(messageJson); + if (request == null) + { + Trace.Warning("Failed to deserialize DAP request"); + return; + } + + if (!string.Equals(request.Type, "request", StringComparison.OrdinalIgnoreCase)) + { + Trace.Warning("Received DAP message that was not a request"); + return; + } + + Trace.Info("Handling DAP request"); + + Response response; + if (request.Command == "evaluate") + { + response = await HandleEvaluateAsync(request, cancellationToken); + } + else + { + response = request.Command switch + { + "initialize" => HandleInitialize(request), + "attach" => HandleAttach(request), + "configurationDone" => HandleConfigurationDone(request), + "disconnect" => HandleDisconnect(request), + "threads" => HandleThreads(request), + "stackTrace" => HandleStackTrace(request), + "scopes" => HandleScopes(request), + "variables" => HandleVariables(request), + "continue" => HandleContinue(request), + "next" => HandleNext(request), + "setBreakpoints" => HandleSetBreakpoints(request), + "setExceptionBreakpoints" => HandleSetExceptionBreakpoints(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), + "stepBack" => CreateResponse(request, false, "Step Back is not yet supported.", body: null), + "reverseContinue" => CreateResponse(request, false, "Reverse Continue is not yet supported.", body: null), + "pause" => CreateResponse(request, false, "Pause is not supported. The debugger pauses automatically at step boundaries.", body: null), + _ => CreateResponse(request, false, $"Unsupported command: {request.Command}", body: null) + }; + } + + response.RequestSeq = request.Seq; + response.Command = request.Command; + + SendResponse(response); + + if (request.Command == "initialize") + { + SendEvent(new Event + { + EventType = "initialized" + }); + Trace.Info("Sent initialized event"); + } + } + catch (Exception ex) + { + Trace.Error($"Error handling DAP request ({ex.GetType().Name})"); + if (request != null) + { + var maskedMessage = HostContext?.SecretMasker?.MaskSecrets(ex.Message) ?? ex.Message; + var errorResponse = CreateResponse(request, false, maskedMessage, body: null); + errorResponse.RequestSeq = request.Seq; + errorResponse.Command = request.Command; + SendResponse(errorResponse); + } + } + } + + internal void HandleClientConnected() + { + _isClientConnected = true; + Trace.Info("Client connected to debug session"); + + // If we're paused, re-send the stopped event so the new client + // knows the current state (important for reconnection) + string description = null; + lock (_stateLock) + { + if (_state == DapSessionState.Paused && _currentStep != null) + { + description = $"Stopped before step: {_currentStep.DisplayName}"; + } + } + + if (description != null) + { + Trace.Info("Re-sending stopped event to reconnected client"); + SendStoppedEvent("step", description); + } + } + + internal void HandleClientDisconnected() + { + _isClientConnected = false; + Trace.Info("Client disconnected from debug session"); + + // Intentionally do NOT release the command TCS here. + // The session stays paused, waiting for a client to reconnect. + // The debugger's connection loop will accept a new client and + // call HandleClientConnected, which re-sends the stopped event. + } + + private async Task ConnectionLoopAsync(CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + Trace.Info("Waiting for debug client connection..."); + _client = await _listener.AcceptTcpClientAsync(); + + if (cancellationToken.IsCancellationRequested) + { + break; + } + + _stream = _client.GetStream(); + var remoteEndPoint = _client.Client.RemoteEndPoint; + Trace.Info($"Debug client connected from {remoteEndPoint}"); + + HandleClientConnected(); + + // Enter message processing loop until client disconnects or cancellation is requested + await ProcessMessagesAsync(cancellationToken); + + Trace.Info("Client disconnected, waiting for reconnection..."); + HandleClientDisconnected(); + CleanupConnection(); + } + catch (Exception ex) + { + CleanupConnection(); + + if (cancellationToken.IsCancellationRequested) + { + break; + } + + Trace.Error("Debugger connection error"); + Trace.Error(ex); + + try + { + await Task.Delay(_connectionRetryDelayMilliseconds, cancellationToken); + } + catch (OperationCanceledException) + { + break; + } + } + } + + Trace.Info("Connection loop ended"); + } + + private void CleanupConnection() + { + _sendLock.Wait(); + try + { + try { _stream?.Close(); } catch { /* best effort */ } + try { _client?.Close(); } catch { /* best effort */ } + _stream = null; + _client = null; + } + finally + { + _sendLock.Release(); + } + } + + private async Task ProcessMessagesAsync(CancellationToken cancellationToken) + { + Trace.Info("Starting DAP message processing loop"); + + try + { + while (!cancellationToken.IsCancellationRequested && _client?.Connected == true) + { + var json = await ReadMessageAsync(cancellationToken); + if (json == null) + { + Trace.Info("Client disconnected (end of stream)"); + break; + } + + await HandleMessageAsync(json, cancellationToken); + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + Trace.Info("Message processing cancelled"); + } + catch (IOException ex) + { + Trace.Info($"Connection closed ({ex.GetType().Name})"); + } + catch (Exception ex) + { + Trace.Error($"Error in message loop ({ex.GetType().Name})"); + } + + Trace.Info("DAP message processing loop ended"); + } + + private async Task ReadMessageAsync(CancellationToken cancellationToken) + { + int contentLength = -1; + + while (true) + { + var line = await ReadLineAsync(cancellationToken); + if (line == null) + { + return null; + } + + if (line.Length == 0) + { + break; + } + + if (line.StartsWith(_contentLengthHeader, StringComparison.OrdinalIgnoreCase)) + { + var lengthStr = line.Substring(_contentLengthHeader.Length).Trim(); + if (!int.TryParse(lengthStr, out contentLength)) + { + throw new InvalidDataException($"Invalid Content-Length: {lengthStr}"); + } + } + } + + if (contentLength < 0) + { + throw new InvalidDataException("Missing Content-Length header"); + } + + if (contentLength > _maxMessageSize) + { + throw new InvalidDataException($"Message size {contentLength} exceeds maximum allowed size of {_maxMessageSize}"); + } + + var buffer = new byte[contentLength]; + var totalRead = 0; + while (totalRead < contentLength) + { + var bytesRead = await _stream.ReadAsync(buffer, totalRead, contentLength - totalRead, cancellationToken); + if (bytesRead == 0) + { + throw new EndOfStreamException("Connection closed while reading message body"); + } + totalRead += bytesRead; + } + + var json = Encoding.UTF8.GetString(buffer); + Trace.Verbose("Received DAP message body"); + return json; + } + + private async Task ReadLineAsync(CancellationToken cancellationToken) + { + var lineBuilder = new StringBuilder(); + var buffer = new byte[1]; + var previousWasCr = false; + + while (true) + { + var bytesRead = await _stream.ReadAsync(buffer, 0, 1, cancellationToken); + if (bytesRead == 0) + { + return lineBuilder.Length > 0 ? lineBuilder.ToString() : null; + } + + var c = (char)buffer[0]; + + if (c == '\n' && previousWasCr) + { + if (lineBuilder.Length > 0 && lineBuilder[lineBuilder.Length - 1] == '\r') + { + lineBuilder.Length--; + } + return lineBuilder.ToString(); + } + + previousWasCr = c == '\r'; + lineBuilder.Append(c); + + if (lineBuilder.Length > _maxHeaderLineLength) + { + throw new InvalidDataException($"Header line exceeds maximum length of {_maxHeaderLineLength}"); + } + } + } + + /// + /// Serializes and writes a DAP message with Content-Length framing. + /// Must be called within the _sendLock. + /// + /// Secret masking is intentionally NOT applied here at the serialization + /// layer. Masking the raw JSON would corrupt protocol envelope fields + /// (type, event, command, seq) if a secret collides with those strings. + /// Instead, each DAP producer masks user-visible text at the point of + /// construction via the runner's SecretMasker. See DapVariableProvider, + /// DapReplExecutor, and DapDebugger for the call sites. + /// + private void SendMessageInternal(ProtocolMessage message) + { + var json = JsonConvert.SerializeObject(message, new JsonSerializerSettings + { + NullValueHandling = NullValueHandling.Ignore + }); + + var bodyBytes = Encoding.UTF8.GetBytes(json); + var header = $"Content-Length: {bodyBytes.Length}\r\n\r\n"; + var headerBytes = Encoding.ASCII.GetBytes(header); + + _stream.Write(headerBytes, 0, headerBytes.Length); + _stream.Write(bodyBytes, 0, bodyBytes.Length); + _stream.Flush(); + + Trace.Verbose("Sent DAP message"); + } + + private void SendMessage(ProtocolMessage message) + { + try + { + _sendLock.Wait(); + try + { + if (_stream == null) + { + Trace.Warning("Cannot send message: no client connected"); + return; + } + + message.Seq = _nextSeq++; + SendMessageInternal(message); + } + finally + { + _sendLock.Release(); + } + + Trace.Info("Sent message"); + } + catch (Exception ex) + { + Trace.Warning($"Failed to send message ({ex.GetType().Name})"); + } + } + + private void SendEvent(Event evt) + { + SendMessage(evt); + } + + private void SendResponse(Response response) + { + SendMessage(response); + } + + private void SendOutput(string category, string text) + { + SendEvent(new Event + { + EventType = "output", + Body = new OutputEventBody + { + Category = category, + Output = text + } + }); + } + + internal async Task OnStepStartingAsync(IStep step, bool isFirstStep) + { + bool pauseOnNextStep; + CancellationToken cancellationToken; + lock (_stateLock) + { + if (_state != DapSessionState.Ready && + _state != DapSessionState.Paused && + _state != DapSessionState.Running) + { + return; + } + + _currentStep = step; + _currentStepIndex = _completedSteps.Count; + pauseOnNextStep = _pauseOnNextStep; + cancellationToken = _jobContext?.CancellationToken ?? CancellationToken.None; + } + + // Reset variable references so stale nested refs from the + // previous step are not served to the client. + _variableProvider?.Reset(); + + // Determine if we should pause + bool shouldPause = isFirstStep || pauseOnNextStep; + + if (!shouldPause) + { + Trace.Info("Step starting without debugger pause"); + return; + } + + var reason = isFirstStep ? "entry" : "step"; + var description = isFirstStep + ? $"Stopped at job entry: {step.DisplayName}" + : $"Stopped before step: {step.DisplayName}"; + + Trace.Info("Step starting with debugger pause"); + + // Send stopped event to debugger (only if client is connected) + SendStoppedEvent(reason, description); + + // Wait for debugger command + await WaitForCommandAsync(cancellationToken); + } + + internal void OnJobCompleted() + { + Trace.Info("Job completed, sending terminated event"); + + int exitCode; + lock (_stateLock) + { + if (_state == DapSessionState.Terminated) + { + Trace.Info("Session already terminated, skipping OnJobCompleted events"); + return; + } + _state = DapSessionState.Terminated; + exitCode = _jobContext?.Result == TaskResult.Succeeded ? 0 : 1; + } + + SendEvent(new Event + { + EventType = "terminated", + Body = new TerminatedEventBody() + }); + + SendEvent(new Event + { + EventType = "exited", + Body = new ExitedEventBody + { + ExitCode = exitCode + } + }); + } + + private Response HandleInitialize(Request request) + { + if (request.Arguments != null) + { + try + { + request.Arguments.ToObject(); + Trace.Info("Initialize arguments received"); + } + catch (Exception ex) + { + Trace.Warning($"Failed to parse initialize arguments ({ex.GetType().Name})"); + } + } + + lock (_stateLock) + { + _state = DapSessionState.Initializing; + } + + // Build capabilities — MVP only supports configurationDone + var capabilities = new Capabilities + { + SupportsConfigurationDoneRequest = true, + SupportsEvaluateForHovers = true, + + // All other capabilities are false for MVP + SupportsFunctionBreakpoints = false, + SupportsConditionalBreakpoints = false, + SupportsStepBack = false, + SupportsSetVariable = false, + SupportsRestartFrame = false, + SupportsGotoTargetsRequest = false, + SupportsStepInTargetsRequest = false, + SupportsCompletionsRequest = true, + SupportsModulesRequest = false, + SupportsTerminateRequest = false, + SupportTerminateDebuggee = false, + SupportsDelayedStackTraceLoading = false, + SupportsLoadedSourcesRequest = false, + SupportsProgressReporting = false, + SupportsRunInTerminalRequest = false, + SupportsCancelRequest = false, + SupportsExceptionOptions = false, + SupportsValueFormattingOptions = false, + SupportsExceptionInfoRequest = false, + }; + + Trace.Info("Initialize request handled, capabilities sent"); + return CreateResponse(request, true, body: capabilities); + } + + private Response HandleAttach(Request request) + { + Trace.Info("Attach request handled"); + return CreateResponse(request, true, body: null); + } + + private Response HandleConfigurationDone(Request request) + { + lock (_stateLock) + { + _state = DapSessionState.Ready; + } + + _readyTcs.TrySetResult(true); + + Trace.Info("Configuration done, debug session is ready"); + return CreateResponse(request, true, body: null); + } + + private Response HandleDisconnect(Request request) + { + Trace.Info("Disconnect request received"); + + lock (_stateLock) + { + _state = DapSessionState.Terminated; + + // Release any blocked step execution + _commandTcs?.TrySetResult(DapCommand.Disconnect); + } + + return CreateResponse(request, true, body: null); + } + + private Response HandleThreads(Request request) + { + IExecutionContext jobContext; + lock (_stateLock) + { + jobContext = _jobContext; + } + + var threadName = jobContext != null + ? MaskUserVisibleText($"Job: {jobContext.GetGitHubContext("job") ?? "workflow job"}") + : "Job Thread"; + + var body = new ThreadsResponseBody + { + Threads = new List + { + new Thread + { + Id = _jobThreadId, + Name = threadName + } + } + }; + + return CreateResponse(request, true, body: body); + } + + private Response HandleStackTrace(Request request) + { + IStep currentStep; + int currentStepIndex; + CompletedStepInfo[] completedSteps; + lock (_stateLock) + { + currentStep = _currentStep; + currentStepIndex = _currentStepIndex; + completedSteps = _completedSteps.ToArray(); + } + + var frames = new List(); + + // Add current step as the top frame + if (currentStep != null) + { + var resultIndicator = currentStep.ExecutionContext?.Result != null + ? $" [{currentStep.ExecutionContext.Result}]" + : " [running]"; + + frames.Add(new StackFrame + { + Id = _currentFrameId, + Name = MaskUserVisibleText($"{currentStep.DisplayName ?? "Current Step"}{resultIndicator}"), + Line = currentStepIndex + 1, + Column = 1, + PresentationHint = "normal" + }); + } + else + { + frames.Add(new StackFrame + { + Id = _currentFrameId, + Name = "(no step executing)", + Line = 0, + Column = 1, + PresentationHint = "subtle" + }); + } + + // Add completed steps as additional frames (most recent first) + for (int i = completedSteps.Length - 1; i >= 0; i--) + { + var completedStep = completedSteps[i]; + var resultStr = completedStep.Result.HasValue ? $" [{completedStep.Result}]" : ""; + frames.Add(new StackFrame + { + Id = completedStep.FrameId, + Name = MaskUserVisibleText($"{completedStep.DisplayName}{resultStr}"), + Line = 1, + Column = 1, + PresentationHint = "subtle" + }); + } + + var body = new StackTraceResponseBody + { + StackFrames = frames, + TotalFrames = frames.Count + }; + + return CreateResponse(request, true, body: body); + } + + private Response HandleScopes(Request request) + { + var args = request.Arguments?.ToObject(); + var frameId = args?.FrameId ?? _currentFrameId; + + var context = GetExecutionContextForFrame(frameId); + if (context == null) + { + return CreateResponse(request, true, body: new ScopesResponseBody + { + Scopes = new List() + }); + } + + var scopes = _variableProvider.GetScopes(context); + return CreateResponse(request, true, body: new ScopesResponseBody + { + Scopes = scopes + }); + } + + private Response HandleVariables(Request request) + { + var args = request.Arguments?.ToObject(); + var variablesRef = args?.VariablesReference ?? 0; + + var context = GetCurrentExecutionContext(); + if (context == null) + { + return CreateResponse(request, true, body: new VariablesResponseBody + { + Variables = new List() + }); + } + + var variables = _variableProvider.GetVariables(context, variablesRef); + return CreateResponse(request, true, body: new VariablesResponseBody + { + Variables = variables + }); + } + + private async Task HandleEvaluateAsync(Request request, CancellationToken cancellationToken) + { + var args = request.Arguments?.ToObject(); + var expression = args?.Expression ?? string.Empty; + var frameId = args?.FrameId ?? _currentFrameId; + var evalContext = args?.Context ?? "hover"; + + Trace.Info("Evaluate request received"); + + // REPL context -> route through the DSL dispatcher + if (string.Equals(evalContext, "repl", StringComparison.OrdinalIgnoreCase)) + { + var result = await HandleReplInputAsync(expression, frameId, cancellationToken); + return CreateResponse(request, true, body: result); + } + + // Watch/hover/variables/clipboard -> expression evaluation only + var context = GetExecutionContextForFrame(frameId); + var evalResult = _variableProvider.EvaluateExpression(expression, context); + return CreateResponse(request, true, body: evalResult); + } + + /// + /// Routes REPL input through the DSL parser. If the input matches a + /// known command it is dispatched; otherwise it falls through to + /// expression evaluation. + /// + private async Task HandleReplInputAsync( + string input, + int frameId, + CancellationToken cancellationToken) + { + // Try to parse as a DSL command + var command = DapReplParser.TryParse(input, out var parseError); + + if (parseError != null) + { + return new EvaluateResponseBody + { + Result = parseError, + Type = "error", + VariablesReference = 0 + }; + } + + if (command != null) + { + return await DispatchReplCommandAsync(command, frameId, cancellationToken); + } + + // Not a DSL command -> evaluate as a GitHub Actions expression + // (this lets the REPL console also work for ad-hoc expression queries) + var context = GetExecutionContextForFrame(frameId); + return _variableProvider.EvaluateExpression(input, context); + } + + private async Task DispatchReplCommandAsync( + DapReplCommand command, + int frameId, + CancellationToken cancellationToken) + { + switch (command) + { + case HelpCommand help: + var helpText = string.IsNullOrEmpty(help.Topic) + ? DapReplParser.GetGeneralHelp() + : help.Topic.Equals("run", StringComparison.OrdinalIgnoreCase) + ? DapReplParser.GetRunHelp() + : $"Unknown help topic: {help.Topic}. Try: help or help(\"run\")"; + return new EvaluateResponseBody + { + Result = helpText, + Type = "string", + VariablesReference = 0 + }; + + case RunCommand run: + var context = GetExecutionContextForFrame(frameId); + return await _replExecutor.ExecuteRunCommandAsync(run, context, cancellationToken); + + default: + return new EvaluateResponseBody + { + Result = $"Unknown command type: {command.GetType().Name}", + Type = "error", + VariablesReference = 0 + }; + } + } + + private Response HandleCompletions(Request request) + { + var args = request.Arguments?.ToObject(); + var text = args?.Text ?? string.Empty; + + var items = new List(); + + // Offer DSL commands when the user is starting to type + if (string.IsNullOrEmpty(text) || "help".StartsWith(text, StringComparison.OrdinalIgnoreCase)) + { + items.Add(new CompletionItem + { + Label = "help", + Text = "help", + Detail = "Show available debug console commands", + Type = "function" + }); + } + if (string.IsNullOrEmpty(text) || "help(\"run\")".StartsWith(text, StringComparison.OrdinalIgnoreCase)) + { + items.Add(new CompletionItem + { + Label = "help(\"run\")", + Text = "help(\"run\")", + Detail = "Show help for the run command", + Type = "function" + }); + } + if (string.IsNullOrEmpty(text) || "run(".StartsWith(text, StringComparison.OrdinalIgnoreCase) + || text.StartsWith("run(", StringComparison.OrdinalIgnoreCase)) + { + items.Add(new CompletionItem + { + Label = "run(\"...\")", + Text = "run(\"", + Detail = "Execute a script (like a workflow run step)", + Type = "function" + }); + } + + return CreateResponse(request, true, body: new CompletionsResponseBody + { + Targets = items + }); + } + + private Response HandleContinue(Request request) + { + Trace.Info("Continue command received"); + + lock (_stateLock) + { + if (_state == DapSessionState.Paused) + { + _state = DapSessionState.Running; + _pauseOnNextStep = false; + _commandTcs?.TrySetResult(DapCommand.Continue); + } + } + + return CreateResponse(request, true, body: new ContinueResponseBody + { + AllThreadsContinued = true + }); + } + + private Response HandleNext(Request request) + { + Trace.Info("Next (step over) command received"); + + lock (_stateLock) + { + if (_state == DapSessionState.Paused) + { + _state = DapSessionState.Running; + _pauseOnNextStep = true; + _commandTcs?.TrySetResult(DapCommand.Next); + } + } + + return CreateResponse(request, true, body: null); + } + + private Response HandleSetBreakpoints(Request request) + { + // MVP: acknowledge but don't process breakpoints + // All steps pause automatically via _pauseOnNextStep + return CreateResponse(request, true, body: new { breakpoints = Array.Empty() }); + } + + private Response HandleSetExceptionBreakpoints(Request request) + { + // MVP: acknowledge but don't process exception breakpoints + return CreateResponse(request, true, body: null); + } + + /// + /// Blocks the step execution thread until a debugger command is received + /// or the job is cancelled. Job cancellation is handled by the registration + /// in StartAsync which sets _commandTcs to Disconnect. + /// + private async Task WaitForCommandAsync(CancellationToken cancellationToken) + { + lock (_stateLock) + { + if (_state == DapSessionState.Terminated) + { + return; + } + _state = DapSessionState.Paused; + _commandTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } + + Trace.Info("Waiting for debugger command..."); + + var command = await _commandTcs.Task; + + Trace.Info("Received debugger command"); + + lock (_stateLock) + { + if (_state == DapSessionState.Paused) + { + _state = DapSessionState.Running; + } + } + + // Send continued event for normal flow commands + if (!cancellationToken.IsCancellationRequested && + (command == DapCommand.Continue || command == DapCommand.Next)) + { + SendEvent(new Event + { + EventType = "continued", + Body = new ContinuedEventBody + { + ThreadId = _jobThreadId, + AllThreadsContinued = true + } + }); + } + } + + /// + /// Resolves the execution context for a given stack frame ID. + /// Frame 1 = current step; frames 1000+ = completed steps (no + /// context available - those steps have already finished). + /// Falls back to the job-level context when no step is active. + /// + private IExecutionContext GetExecutionContextForFrame(int frameId) + { + if (frameId == _currentFrameId) + { + return GetCurrentExecutionContext(); + } + + // Completed-step frames don't carry a live execution context. + return null; + } + + private IExecutionContext GetCurrentExecutionContext() + { + lock (_stateLock) + { + return _currentStep?.ExecutionContext ?? _jobContext; + } + } + + /// + /// Sends a stopped event to the connected client. + /// Silently no-ops if no client is connected. + /// + private void SendStoppedEvent(string reason, string description) + { + if (!_isClientConnected) + { + Trace.Info("No client connected, deferring stopped event"); + return; + } + + SendEvent(new Event + { + EventType = "stopped", + Body = new StoppedEventBody + { + Reason = reason, + Description = MaskUserVisibleText(description), + ThreadId = _jobThreadId, + AllThreadsStopped = true + } + }); + } + + private string MaskUserVisibleText(string value) + { + if (string.IsNullOrEmpty(value)) + { + return value ?? string.Empty; + } + + return HostContext?.SecretMasker?.MaskSecrets(value) ?? value; + } + + /// + /// Creates a DAP response with common fields pre-populated. + /// + private Response CreateResponse(Request request, bool success, string message = null, object body = null) + { + return new Response + { + Type = "response", + RequestSeq = request.Seq, + Command = request.Command, + Success = success, + Message = success ? null : message, + Body = body + }; + } + + internal int ResolvePort() + { + var portEnv = Environment.GetEnvironmentVariable(_portEnvironmentVariable); + if (!string.IsNullOrEmpty(portEnv) && int.TryParse(portEnv, out var customPort) && customPort > 1024 && customPort <= 65535) + { + Trace.Info($"Using custom DAP port {customPort} from {_portEnvironmentVariable}"); + return customPort; + } + + return _defaultPort; + } + + internal int ResolveTimeout() + { + var timeoutEnv = Environment.GetEnvironmentVariable(_timeoutEnvironmentVariable); + if (!string.IsNullOrEmpty(timeoutEnv) && int.TryParse(timeoutEnv, out var customTimeout) && customTimeout > 0) + { + Trace.Info($"Using custom DAP timeout {customTimeout} minutes from {_timeoutEnvironmentVariable}"); + return customTimeout; + } + + return _defaultTimeoutMinutes; + } + } +} diff --git a/src/Runner.Worker/Dap/DapMessages.cs b/src/Runner.Worker/Dap/DapMessages.cs new file mode 100644 index 000000000..53cd7a436 --- /dev/null +++ b/src/Runner.Worker/Dap/DapMessages.cs @@ -0,0 +1,1231 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace GitHub.Runner.Worker.Dap +{ + public enum DapCommand + { + Continue, + Next, + StepIn, + StepOut, + Disconnect + } + + /// + /// Base class of requests, responses, and events per DAP specification. + /// + public class ProtocolMessage + { + /// + /// Sequence number of the message (also known as message ID). + /// The seq for the first message sent by a client or debug adapter is 1, + /// and for each subsequent message is 1 greater than the previous message. + /// + [JsonProperty("seq")] + public int Seq { get; set; } + + /// + /// Message type: 'request', 'response', 'event' + /// + [JsonProperty("type")] + public string Type { get; set; } + } + + /// + /// A client or debug adapter initiated request. + /// + public class Request : ProtocolMessage + { + /// + /// The command to execute. + /// + [JsonProperty("command")] + public string Command { get; set; } + + /// + /// Object containing arguments for the command. + /// Using JObject for flexibility with different argument types. + /// + [JsonProperty("arguments")] + public JObject Arguments { get; set; } + } + + /// + /// Response for a request. + /// + public class Response : ProtocolMessage + { + /// + /// Sequence number of the corresponding request. + /// + [JsonProperty("request_seq")] + public int RequestSeq { get; set; } + + /// + /// Outcome of the request. If true, the request was successful. + /// + [JsonProperty("success")] + public bool Success { get; set; } + + /// + /// The command requested. + /// + [JsonProperty("command")] + public string Command { get; set; } + + /// + /// Contains the raw error in short form if success is false. + /// + [JsonProperty("message", NullValueHandling = NullValueHandling.Ignore)] + public string Message { get; set; } + + /// + /// Contains request result if success is true and error details if success is false. + /// + [JsonProperty("body", NullValueHandling = NullValueHandling.Ignore)] + public object Body { get; set; } + } + + /// + /// A debug adapter initiated event. + /// + public class Event : ProtocolMessage + { + public Event() + { + Type = "event"; + } + + /// + /// Type of event. + /// + [JsonProperty("event")] + public string EventType { get; set; } + + /// + /// Event-specific information. + /// + [JsonProperty("body", NullValueHandling = NullValueHandling.Ignore)] + public object Body { get; set; } + } + + #region Initialize Request/Response + + /// + /// Arguments for 'initialize' request. + /// + public class InitializeRequestArguments + { + /// + /// The ID of the client using this adapter. + /// + [JsonProperty("clientID")] + public string ClientId { get; set; } + + /// + /// The human-readable name of the client using this adapter. + /// + [JsonProperty("clientName")] + public string ClientName { get; set; } + + /// + /// The ID of the debug adapter. + /// + [JsonProperty("adapterID")] + public string AdapterId { get; set; } + + /// + /// The ISO-639 locale of the client using this adapter, e.g. en-US or de-CH. + /// + [JsonProperty("locale")] + public string Locale { get; set; } + + /// + /// If true all line numbers are 1-based (default). + /// + [JsonProperty("linesStartAt1")] + public bool LinesStartAt1 { get; set; } = true; + + /// + /// If true all column numbers are 1-based (default). + /// + [JsonProperty("columnsStartAt1")] + public bool ColumnsStartAt1 { get; set; } = true; + + /// + /// Determines in what format paths are specified. The default is 'path'. + /// + [JsonProperty("pathFormat")] + public string PathFormat { get; set; } = "path"; + + /// + /// Client supports the type attribute for variables. + /// + [JsonProperty("supportsVariableType")] + public bool SupportsVariableType { get; set; } + + /// + /// Client supports the paging of variables. + /// + [JsonProperty("supportsVariablePaging")] + public bool SupportsVariablePaging { get; set; } + + /// + /// Client supports the runInTerminal request. + /// + [JsonProperty("supportsRunInTerminalRequest")] + public bool SupportsRunInTerminalRequest { get; set; } + + /// + /// Client supports memory references. + /// + [JsonProperty("supportsMemoryReferences")] + public bool SupportsMemoryReferences { get; set; } + + /// + /// Client supports progress reporting. + /// + [JsonProperty("supportsProgressReporting")] + public bool SupportsProgressReporting { get; set; } + } + + /// + /// Debug adapter capabilities returned in InitializeResponse. + /// + public class Capabilities + { + /// + /// The debug adapter supports the configurationDone request. + /// + [JsonProperty("supportsConfigurationDoneRequest")] + public bool SupportsConfigurationDoneRequest { get; set; } + + /// + /// The debug adapter supports function breakpoints. + /// + [JsonProperty("supportsFunctionBreakpoints")] + public bool SupportsFunctionBreakpoints { get; set; } + + /// + /// The debug adapter supports conditional breakpoints. + /// + [JsonProperty("supportsConditionalBreakpoints")] + public bool SupportsConditionalBreakpoints { get; set; } + + /// + /// The debug adapter supports a (side effect free) evaluate request for data hovers. + /// + [JsonProperty("supportsEvaluateForHovers")] + public bool SupportsEvaluateForHovers { get; set; } + + /// + /// The debug adapter supports stepping back via the stepBack and reverseContinue requests. + /// + [JsonProperty("supportsStepBack")] + public bool SupportsStepBack { get; set; } + + /// + /// The debug adapter supports setting a variable to a value. + /// + [JsonProperty("supportsSetVariable")] + public bool SupportsSetVariable { get; set; } + + /// + /// The debug adapter supports restarting a frame. + /// + [JsonProperty("supportsRestartFrame")] + public bool SupportsRestartFrame { get; set; } + + /// + /// The debug adapter supports the gotoTargets request. + /// + [JsonProperty("supportsGotoTargetsRequest")] + public bool SupportsGotoTargetsRequest { get; set; } + + /// + /// The debug adapter supports the stepInTargets request. + /// + [JsonProperty("supportsStepInTargetsRequest")] + public bool SupportsStepInTargetsRequest { get; set; } + + /// + /// The debug adapter supports the completions request. + /// + [JsonProperty("supportsCompletionsRequest")] + public bool SupportsCompletionsRequest { get; set; } + + /// + /// The debug adapter supports the modules request. + /// + [JsonProperty("supportsModulesRequest")] + public bool SupportsModulesRequest { get; set; } + + /// + /// The debug adapter supports the terminate request. + /// + [JsonProperty("supportsTerminateRequest")] + public bool SupportsTerminateRequest { get; set; } + + /// + /// The debug adapter supports the terminateDebuggee attribute on the disconnect request. + /// + [JsonProperty("supportTerminateDebuggee")] + public bool SupportTerminateDebuggee { get; set; } + + /// + /// The debug adapter supports the delayed loading of parts of the stack. + /// + [JsonProperty("supportsDelayedStackTraceLoading")] + public bool SupportsDelayedStackTraceLoading { get; set; } + + /// + /// The debug adapter supports the loadedSources request. + /// + [JsonProperty("supportsLoadedSourcesRequest")] + public bool SupportsLoadedSourcesRequest { get; set; } + + /// + /// The debug adapter supports sending progress reporting events. + /// + [JsonProperty("supportsProgressReporting")] + public bool SupportsProgressReporting { get; set; } + + /// + /// The debug adapter supports the runInTerminal request. + /// + [JsonProperty("supportsRunInTerminalRequest")] + public bool SupportsRunInTerminalRequest { get; set; } + + /// + /// The debug adapter supports the cancel request. + /// + [JsonProperty("supportsCancelRequest")] + public bool SupportsCancelRequest { get; set; } + + /// + /// The debug adapter supports exception options. + /// + [JsonProperty("supportsExceptionOptions")] + public bool SupportsExceptionOptions { get; set; } + + /// + /// The debug adapter supports value formatting options. + /// + [JsonProperty("supportsValueFormattingOptions")] + public bool SupportsValueFormattingOptions { get; set; } + + /// + /// The debug adapter supports exception info request. + /// + [JsonProperty("supportsExceptionInfoRequest")] + public bool SupportsExceptionInfoRequest { get; set; } + } + + #endregion + + #region Attach Request + + /// + /// Arguments for 'attach' request. Additional attributes are implementation specific. + /// + public class AttachRequestArguments + { + /// + /// Arbitrary data from the previous, restarted session. + /// + [JsonProperty("__restart", NullValueHandling = NullValueHandling.Ignore)] + public object Restart { get; set; } + } + + #endregion + + #region Disconnect Request + + /// + /// Arguments for 'disconnect' request. + /// + public class DisconnectRequestArguments + { + /// + /// A value of true indicates that this disconnect request is part of a restart sequence. + /// + [JsonProperty("restart")] + public bool Restart { get; set; } + + /// + /// Indicates whether the debuggee should be terminated when the debugger is disconnected. + /// + [JsonProperty("terminateDebuggee")] + public bool TerminateDebuggee { get; set; } + + /// + /// Indicates whether the debuggee should stay suspended when the debugger is disconnected. + /// + [JsonProperty("suspendDebuggee")] + public bool SuspendDebuggee { get; set; } + } + + #endregion + + #region Threads Request/Response + + /// + /// A Thread in DAP represents a unit of execution. + /// For Actions runner, we have a single thread representing the job. + /// + public class Thread + { + /// + /// Unique identifier for the thread. + /// + [JsonProperty("id")] + public int Id { get; set; } + + /// + /// The name of the thread. + /// + [JsonProperty("name")] + public string Name { get; set; } + } + + /// + /// Response body for 'threads' request. + /// + public class ThreadsResponseBody + { + /// + /// All threads. + /// + [JsonProperty("threads")] + public List Threads { get; set; } = new List(); + } + + #endregion + + #region StackTrace Request/Response + + /// + /// Arguments for 'stackTrace' request. + /// + public class StackTraceArguments + { + /// + /// Retrieve the stacktrace for this thread. + /// + [JsonProperty("threadId")] + public int ThreadId { get; set; } + + /// + /// The index of the first frame to return. + /// + [JsonProperty("startFrame")] + public int? StartFrame { get; set; } + + /// + /// The maximum number of frames to return. + /// + [JsonProperty("levels")] + public int? Levels { get; set; } + } + + /// + /// A Stackframe contains the source location. + /// For Actions runner, each step is a stack frame. + /// + public class StackFrame + { + /// + /// An identifier for the stack frame. + /// + [JsonProperty("id")] + public int Id { get; set; } + + /// + /// The name of the stack frame, typically a method name. + /// For Actions, this is the step display name. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// The source of the frame. + /// + [JsonProperty("source", NullValueHandling = NullValueHandling.Ignore)] + public Source Source { get; set; } + + /// + /// The line within the source of the frame. + /// + [JsonProperty("line")] + public int Line { get; set; } + + /// + /// Start position of the range covered by the stack frame. + /// + [JsonProperty("column")] + public int Column { get; set; } + + /// + /// The end line of the range covered by the stack frame. + /// + [JsonProperty("endLine", NullValueHandling = NullValueHandling.Ignore)] + public int? EndLine { get; set; } + + /// + /// End position of the range covered by the stack frame. + /// + [JsonProperty("endColumn", NullValueHandling = NullValueHandling.Ignore)] + public int? EndColumn { get; set; } + + /// + /// A hint for how to present this frame in the UI. + /// + [JsonProperty("presentationHint", NullValueHandling = NullValueHandling.Ignore)] + public string PresentationHint { get; set; } + } + + /// + /// A Source is a descriptor for source code. + /// + public class Source + { + /// + /// The short name of the source. + /// + [JsonProperty("name", NullValueHandling = NullValueHandling.Ignore)] + public string Name { get; set; } + + /// + /// The path of the source to be shown in the UI. + /// + [JsonProperty("path", NullValueHandling = NullValueHandling.Ignore)] + public string Path { get; set; } + + /// + /// If the value > 0 the contents of the source must be retrieved through + /// the 'source' request (even if a path is specified). + /// + [JsonProperty("sourceReference", NullValueHandling = NullValueHandling.Ignore)] + public int? SourceReference { get; set; } + + /// + /// A hint for how to present the source in the UI. + /// + [JsonProperty("presentationHint", NullValueHandling = NullValueHandling.Ignore)] + public string PresentationHint { get; set; } + } + + /// + /// Response body for 'stackTrace' request. + /// + public class StackTraceResponseBody + { + /// + /// The frames of the stack frame. + /// + [JsonProperty("stackFrames")] + public List StackFrames { get; set; } = new List(); + + /// + /// The total number of frames available in the stack. + /// + [JsonProperty("totalFrames", NullValueHandling = NullValueHandling.Ignore)] + public int? TotalFrames { get; set; } + } + + #endregion + + #region Scopes Request/Response + + /// + /// Arguments for 'scopes' request. + /// + public class ScopesArguments + { + /// + /// Retrieve the scopes for the stack frame identified by frameId. + /// + [JsonProperty("frameId")] + public int FrameId { get; set; } + } + + /// + /// A Scope is a named container for variables. + /// For Actions runner, scopes are: github, env, inputs, steps, secrets, runner, job + /// + public class Scope + { + /// + /// Name of the scope such as 'Arguments', 'Locals', or 'Registers'. + /// For Actions: 'github', 'env', 'inputs', 'steps', 'secrets', 'runner', 'job' + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// A hint for how to present this scope in the UI. + /// + [JsonProperty("presentationHint", NullValueHandling = NullValueHandling.Ignore)] + public string PresentationHint { get; set; } + + /// + /// The variables of this scope can be retrieved by passing the value of + /// variablesReference to the variables request. + /// + [JsonProperty("variablesReference")] + public int VariablesReference { get; set; } + + /// + /// The number of named variables in this scope. + /// + [JsonProperty("namedVariables", NullValueHandling = NullValueHandling.Ignore)] + public int? NamedVariables { get; set; } + + /// + /// The number of indexed variables in this scope. + /// + [JsonProperty("indexedVariables", NullValueHandling = NullValueHandling.Ignore)] + public int? IndexedVariables { get; set; } + + /// + /// If true, the number of variables in this scope is large or expensive to retrieve. + /// + [JsonProperty("expensive")] + public bool Expensive { get; set; } + + /// + /// The source for this scope. + /// + [JsonProperty("source", NullValueHandling = NullValueHandling.Ignore)] + public Source Source { get; set; } + + /// + /// The start line of the range covered by this scope. + /// + [JsonProperty("line", NullValueHandling = NullValueHandling.Ignore)] + public int? Line { get; set; } + + /// + /// Start position of the range covered by this scope. + /// + [JsonProperty("column", NullValueHandling = NullValueHandling.Ignore)] + public int? Column { get; set; } + + /// + /// The end line of the range covered by this scope. + /// + [JsonProperty("endLine", NullValueHandling = NullValueHandling.Ignore)] + public int? EndLine { get; set; } + + /// + /// End position of the range covered by this scope. + /// + [JsonProperty("endColumn", NullValueHandling = NullValueHandling.Ignore)] + public int? EndColumn { get; set; } + } + + /// + /// Response body for 'scopes' request. + /// + public class ScopesResponseBody + { + /// + /// The scopes of the stack frame. + /// + [JsonProperty("scopes")] + public List Scopes { get; set; } = new List(); + } + + #endregion + + #region Variables Request/Response + + /// + /// Arguments for 'variables' request. + /// + public class VariablesArguments + { + /// + /// The variable for which to retrieve its children. + /// + [JsonProperty("variablesReference")] + public int VariablesReference { get; set; } + + /// + /// Filter to limit the child variables to either named or indexed. + /// + [JsonProperty("filter", NullValueHandling = NullValueHandling.Ignore)] + public string Filter { get; set; } + + /// + /// The index of the first variable to return. + /// + [JsonProperty("start", NullValueHandling = NullValueHandling.Ignore)] + public int? Start { get; set; } + + /// + /// The number of variables to return. + /// + [JsonProperty("count", NullValueHandling = NullValueHandling.Ignore)] + public int? Count { get; set; } + } + + /// + /// A Variable is a name/value pair. + /// + public class Variable + { + /// + /// The variable's name. + /// + [JsonProperty("name")] + public string Name { get; set; } + + /// + /// The variable's value. + /// + [JsonProperty("value")] + public string Value { get; set; } + + /// + /// The type of the variable's value. + /// + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public string Type { get; set; } + + /// + /// If variablesReference is > 0, the variable is structured and its children + /// can be retrieved by passing variablesReference to the variables request. + /// + [JsonProperty("variablesReference")] + public int VariablesReference { get; set; } + + /// + /// The number of named child variables. + /// + [JsonProperty("namedVariables", NullValueHandling = NullValueHandling.Ignore)] + public int? NamedVariables { get; set; } + + /// + /// The number of indexed child variables. + /// + [JsonProperty("indexedVariables", NullValueHandling = NullValueHandling.Ignore)] + public int? IndexedVariables { get; set; } + + /// + /// A memory reference to a location appropriate for this result. + /// + [JsonProperty("memoryReference", NullValueHandling = NullValueHandling.Ignore)] + public string MemoryReference { get; set; } + + /// + /// A reference that allows the client to request the location where the + /// variable's value is declared. + /// + [JsonProperty("declarationLocationReference", NullValueHandling = NullValueHandling.Ignore)] + public int? DeclarationLocationReference { get; set; } + + /// + /// The evaluatable name of this variable which can be passed to the evaluate + /// request to fetch the variable's value. + /// + [JsonProperty("evaluateName", NullValueHandling = NullValueHandling.Ignore)] + public string EvaluateName { get; set; } + } + + /// + /// Response body for 'variables' request. + /// + public class VariablesResponseBody + { + /// + /// All (or a range) of variables for the given variable reference. + /// + [JsonProperty("variables")] + public List Variables { get; set; } = new List(); + } + + #endregion + + #region Continue Request/Response + + /// + /// Arguments for 'continue' request. + /// + public class ContinueArguments + { + /// + /// Specifies the active thread. If the debug adapter supports single thread + /// execution, setting this will resume only the specified thread. + /// + [JsonProperty("threadId")] + public int ThreadId { get; set; } + + /// + /// If this flag is true, execution is resumed only for the thread with given + /// threadId. If false, all threads are resumed. + /// + [JsonProperty("singleThread")] + public bool SingleThread { get; set; } + } + + /// + /// Response body for 'continue' request. + /// + public class ContinueResponseBody + { + /// + /// If true, all threads are resumed. If false, only the thread with the given + /// threadId is resumed. + /// + [JsonProperty("allThreadsContinued")] + public bool AllThreadsContinued { get; set; } = true; + } + + #endregion + + #region Next Request + + /// + /// Arguments for 'next' request. + /// + public class NextArguments + { + /// + /// Specifies the thread for which to resume execution for one step. + /// + [JsonProperty("threadId")] + public int ThreadId { get; set; } + + /// + /// Stepping granularity. + /// + [JsonProperty("granularity", NullValueHandling = NullValueHandling.Ignore)] + public string Granularity { get; set; } + + /// + /// If this flag is true, all other suspended threads are not resumed. + /// + [JsonProperty("singleThread")] + public bool SingleThread { get; set; } + } + + #endregion + + #region Evaluate Request/Response + + /// + /// Arguments for 'evaluate' request. + /// + public class EvaluateArguments + { + /// + /// The expression to evaluate. + /// + [JsonProperty("expression")] + public string Expression { get; set; } + + /// + /// Evaluate the expression in the scope of this stack frame. + /// + [JsonProperty("frameId", NullValueHandling = NullValueHandling.Ignore)] + public int? FrameId { get; set; } + + /// + /// The context in which the evaluate request is used. + /// Values: 'watch', 'repl', 'hover', 'clipboard', 'variables' + /// + [JsonProperty("context", NullValueHandling = NullValueHandling.Ignore)] + public string Context { get; set; } + } + + /// + /// Response body for 'evaluate' request. + /// + public class EvaluateResponseBody + { + /// + /// The result of the evaluate request. + /// + [JsonProperty("result")] + public string Result { get; set; } + + /// + /// The type of the evaluate result. + /// + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public string Type { get; set; } + + /// + /// If variablesReference is > 0, the evaluate result is structured. + /// + [JsonProperty("variablesReference")] + public int VariablesReference { get; set; } + + /// + /// The number of named child variables. + /// + [JsonProperty("namedVariables", NullValueHandling = NullValueHandling.Ignore)] + public int? NamedVariables { get; set; } + + /// + /// The number of indexed child variables. + /// + [JsonProperty("indexedVariables", NullValueHandling = NullValueHandling.Ignore)] + public int? IndexedVariables { get; set; } + + /// + /// A memory reference to a location appropriate for this result. + /// + [JsonProperty("memoryReference", NullValueHandling = NullValueHandling.Ignore)] + public string MemoryReference { get; set; } + } + + #endregion + + #region Completions Request/Response + + /// + /// Arguments for 'completions' request. + /// + public class CompletionsArguments + { + /// + /// Returns completions in the scope of this stack frame. + /// + [JsonProperty("frameId", NullValueHandling = NullValueHandling.Ignore)] + public int? FrameId { get; set; } + + /// + /// One or more source lines. Typically this is the text users have typed + /// in the debug console (REPL). + /// + [JsonProperty("text")] + public string Text { get; set; } + + /// + /// The position within 'text' for which to determine the completion proposals. + /// It is measured in UTF-16 code units. + /// + [JsonProperty("column")] + public int Column { get; set; } + + /// + /// A line for which to determine the completion proposals. + /// If missing the first line of the text is assumed. + /// + [JsonProperty("line", NullValueHandling = NullValueHandling.Ignore)] + public int? Line { get; set; } + } + + /// + /// A completion item in the debug console. + /// + public class CompletionItem + { + /// + /// The label of this completion item. + /// + [JsonProperty("label")] + public string Label { get; set; } + + /// + /// If text is returned and not an empty string, then it is inserted instead + /// of the label. + /// + [JsonProperty("text", NullValueHandling = NullValueHandling.Ignore)] + public string Text { get; set; } + + /// + /// A human-readable string with additional information about this item. + /// + [JsonProperty("detail", NullValueHandling = NullValueHandling.Ignore)] + public string Detail { get; set; } + + /// + /// The item's type. Typically the client uses this information to render the item + /// in the UI with an icon. + /// Values: 'method', 'function', 'constructor', 'field', 'variable', 'class', + /// 'interface', 'module', 'property', 'unit', 'value', 'enum', 'keyword', + /// 'snippet', 'text', 'color', 'file', 'reference', 'customcolor' + /// + [JsonProperty("type", NullValueHandling = NullValueHandling.Ignore)] + public string Type { get; set; } + + /// + /// Start position (0-based) within 'text' that should be replaced + /// by the completion text. + /// + [JsonProperty("start", NullValueHandling = NullValueHandling.Ignore)] + public int? Start { get; set; } + + /// + /// Length of the text that should be replaced by the completion text. + /// + [JsonProperty("length", NullValueHandling = NullValueHandling.Ignore)] + public int? Length { get; set; } + } + + /// + /// Response body for 'completions' request. + /// + public class CompletionsResponseBody + { + /// + /// The possible completions. + /// + [JsonProperty("targets")] + public List Targets { get; set; } = new List(); + } + + #endregion + + #region Events + + /// + /// Body for 'stopped' event. + /// The event indicates that the execution of the debuggee has stopped. + /// + public class StoppedEventBody + { + /// + /// The reason for the event. For backward compatibility this string is shown + /// in the UI if the description attribute is missing. + /// Values: 'step', 'breakpoint', 'exception', 'pause', 'entry', 'goto', + /// 'function breakpoint', 'data breakpoint', 'instruction breakpoint' + /// + [JsonProperty("reason")] + public string Reason { get; set; } + + /// + /// The full reason for the event, e.g. 'Paused on exception'. + /// This string is shown in the UI as is and can be translated. + /// + [JsonProperty("description", NullValueHandling = NullValueHandling.Ignore)] + public string Description { get; set; } + + /// + /// The thread which was stopped. + /// + [JsonProperty("threadId", NullValueHandling = NullValueHandling.Ignore)] + public int? ThreadId { get; set; } + + /// + /// A value of true hints to the client that this event should not change the focus. + /// + [JsonProperty("preserveFocusHint", NullValueHandling = NullValueHandling.Ignore)] + public bool? PreserveFocusHint { get; set; } + + /// + /// Additional information. E.g. if reason is 'exception', text contains the + /// exception name. + /// + [JsonProperty("text", NullValueHandling = NullValueHandling.Ignore)] + public string Text { get; set; } + + /// + /// If allThreadsStopped is true, a debug adapter can announce that all threads + /// have stopped. + /// + [JsonProperty("allThreadsStopped", NullValueHandling = NullValueHandling.Ignore)] + public bool? AllThreadsStopped { get; set; } + + /// + /// Ids of the breakpoints that triggered the event. + /// + [JsonProperty("hitBreakpointIds", NullValueHandling = NullValueHandling.Ignore)] + public List HitBreakpointIds { get; set; } + } + + /// + /// Body for 'continued' event. + /// The event indicates that the execution of the debuggee has continued. + /// + public class ContinuedEventBody + { + /// + /// The thread which was continued. + /// + [JsonProperty("threadId")] + public int ThreadId { get; set; } + + /// + /// If true, all threads have been resumed. + /// + [JsonProperty("allThreadsContinued", NullValueHandling = NullValueHandling.Ignore)] + public bool? AllThreadsContinued { get; set; } + } + + /// + /// Body for 'terminated' event. + /// The event indicates that debugging of the debuggee has terminated. + /// + public class TerminatedEventBody + { + /// + /// A debug adapter may set restart to true to request that the client + /// restarts the session. + /// + [JsonProperty("restart", NullValueHandling = NullValueHandling.Ignore)] + public object Restart { get; set; } + } + + /// + /// Body for 'output' event. + /// The event indicates that the target has produced some output. + /// + public class OutputEventBody + { + /// + /// The output category. If not specified, 'console' is assumed. + /// Values: 'console', 'important', 'stdout', 'stderr', 'telemetry' + /// + [JsonProperty("category", NullValueHandling = NullValueHandling.Ignore)] + public string Category { get; set; } + + /// + /// The output to report. + /// + [JsonProperty("output")] + public string Output { get; set; } + + /// + /// Support for keeping an output log organized by grouping related messages. + /// Values: 'start', 'startCollapsed', 'end' + /// + [JsonProperty("group", NullValueHandling = NullValueHandling.Ignore)] + public string Group { get; set; } + + /// + /// If variablesReference is > 0, the output contains objects which can be + /// retrieved by passing variablesReference to the variables request. + /// + [JsonProperty("variablesReference", NullValueHandling = NullValueHandling.Ignore)] + public int? VariablesReference { get; set; } + + /// + /// The source location where the output was produced. + /// + [JsonProperty("source", NullValueHandling = NullValueHandling.Ignore)] + public Source Source { get; set; } + + /// + /// The source location's line where the output was produced. + /// + [JsonProperty("line", NullValueHandling = NullValueHandling.Ignore)] + public int? Line { get; set; } + + /// + /// The position in line where the output was produced. + /// + [JsonProperty("column", NullValueHandling = NullValueHandling.Ignore)] + public int? Column { get; set; } + + /// + /// Additional data to report. + /// + [JsonProperty("data", NullValueHandling = NullValueHandling.Ignore)] + public object Data { get; set; } + } + + /// + /// Body for 'thread' event. + /// The event indicates that a thread has started or exited. + /// + public class ThreadEventBody + { + /// + /// The reason for the event. + /// Values: 'started', 'exited' + /// + [JsonProperty("reason")] + public string Reason { get; set; } + + /// + /// The identifier of the thread. + /// + [JsonProperty("threadId")] + public int ThreadId { get; set; } + } + + /// + /// Body for 'exited' event. + /// The event indicates that the debuggee has exited and returns its exit code. + /// + public class ExitedEventBody + { + /// + /// The exit code returned from the debuggee. + /// + [JsonProperty("exitCode")] + public int ExitCode { get; set; } + } + + #endregion + + #region Error Response + + /// + /// A structured error message. + /// + public class Message + { + /// + /// Unique identifier for the message. + /// + [JsonProperty("id")] + public int Id { get; set; } + + /// + /// A format string for the message. + /// + [JsonProperty("format")] + public string Format { get; set; } + + /// + /// An object used as a dictionary for looking up the variables in the format string. + /// + [JsonProperty("variables", NullValueHandling = NullValueHandling.Ignore)] + public Dictionary Variables { get; set; } + + /// + /// If true send to telemetry. + /// + [JsonProperty("sendTelemetry", NullValueHandling = NullValueHandling.Ignore)] + public bool? SendTelemetry { get; set; } + + /// + /// If true show user. + /// + [JsonProperty("showUser", NullValueHandling = NullValueHandling.Ignore)] + public bool? ShowUser { get; set; } + + /// + /// A url where additional information about this message can be found. + /// + [JsonProperty("url", NullValueHandling = NullValueHandling.Ignore)] + public string Url { get; set; } + + /// + /// A label that is presented to the user as the UI for opening the url. + /// + [JsonProperty("urlLabel", NullValueHandling = NullValueHandling.Ignore)] + public string UrlLabel { get; set; } + } + + /// + /// Body for error responses. + /// + public class ErrorResponseBody + { + /// + /// A structured error message. + /// + [JsonProperty("error", NullValueHandling = NullValueHandling.Ignore)] + public Message Error { get; set; } + } + + #endregion +} diff --git a/src/Runner.Worker/Dap/DapReplExecutor.cs b/src/Runner.Worker/Dap/DapReplExecutor.cs new file mode 100644 index 000000000..751f92c51 --- /dev/null +++ b/src/Runner.Worker/Dap/DapReplExecutor.cs @@ -0,0 +1,369 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using GitHub.DistributedTask.Pipelines.ContextData; +using GitHub.Runner.Common; +using GitHub.Runner.Common.Util; +using GitHub.Runner.Sdk; +using GitHub.Runner.Worker.Handlers; + +namespace GitHub.Runner.Worker.Dap +{ + /// + /// Executes objects in the job's runtime context. + /// + /// Mirrors the behavior of a normal workflow run: step as closely + /// as possible by reusing the runner's existing shell-resolution logic, + /// script fixup helpers, and process execution infrastructure. + /// + /// Output is streamed to the debugger via DAP output events with + /// secrets masked before emission. + /// + internal sealed class DapReplExecutor + { + private readonly IHostContext _hostContext; + private readonly Action _sendOutput; + private readonly Tracing _trace; + + public DapReplExecutor(IHostContext hostContext, Action sendOutput) + { + _hostContext = hostContext ?? throw new ArgumentNullException(nameof(hostContext)); + _sendOutput = sendOutput ?? throw new ArgumentNullException(nameof(sendOutput)); + _trace = hostContext.GetTrace(nameof(DapReplExecutor)); + } + + /// + /// Executes a and returns the exit code as a + /// formatted . + /// + public async Task ExecuteRunCommandAsync( + RunCommand command, + IExecutionContext context, + CancellationToken cancellationToken) + { + if (context == null) + { + return ErrorResult("No execution context available. The debugger must be paused at a step to run commands."); + } + + try + { + return await ExecuteScriptAsync(command, context, cancellationToken); + } + catch (Exception ex) + { + _trace.Error($"REPL run command failed ({ex.GetType().Name})"); + var maskedError = _hostContext.SecretMasker.MaskSecrets(ex.Message); + return ErrorResult($"Command failed: {maskedError}"); + } + } + + private async Task ExecuteScriptAsync( + RunCommand command, + IExecutionContext context, + CancellationToken cancellationToken) + { + // 1. Resolve shell — same logic as ScriptHandler + string shellCommand; + string argFormat; + + if (!string.IsNullOrEmpty(command.Shell)) + { + // Explicit shell from the DSL + var parsed = ScriptHandlerHelpers.ParseShellOptionString(command.Shell); + shellCommand = parsed.shellCommand; + argFormat = string.IsNullOrEmpty(parsed.shellArgs) + ? ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand) + : parsed.shellArgs; + } + else + { + // Default shell — mirrors ScriptHandler platform defaults + shellCommand = ResolveDefaultShell(context); + argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand); + } + + _trace.Info("Resolved REPL shell"); + + // 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); + + // Write to a temp file (same pattern as ScriptHandler) + var extension = ScriptHandlerHelpers.GetScriptFileExtension(shellCommand); + var scriptFilePath = Path.Combine( + _hostContext.GetDirectory(WellKnownDirectory.Temp), + $"dap_repl_{Guid.NewGuid()}{extension}"); + + Encoding encoding = new UTF8Encoding(false); +#if OS_WINDOWS + contents = contents.Replace("\r\n", "\n").Replace("\n", "\r\n"); + encoding = Console.InputEncoding.CodePage != 65001 + ? Console.InputEncoding + : encoding; +#endif + File.WriteAllText(scriptFilePath, contents, encoding); + + try + { + // 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); + + // 4. Resolve shell command path + string prependPath = string.Join( + Path.PathSeparator.ToString(), + Enumerable.Reverse(context.Global.PrependPath)); + var commandPath = WhichUtil.Which(shellCommand, false, _trace, prependPath) + ?? shellCommand; + + // 5. Build environment — merge from execution context like a real step + var environment = BuildEnvironment(context, command.Env); + + // 6. Resolve working directory + var workingDirectory = command.WorkingDirectory; + if (string.IsNullOrEmpty(workingDirectory)) + { + var githubContext = context.ExpressionValues.TryGetValue("github", out var gh) + ? gh as DictionaryContextData + : null; + var workspace = githubContext?.TryGetValue("workspace", out var ws) == true + ? (ws as StringContextData)?.Value + : null; + workingDirectory = workspace ?? _hostContext.GetDirectory(WellKnownDirectory.Work); + } + + _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"); + + // 7. Execute via IProcessInvoker (same as DefaultStepHost) + int exitCode; + using (var processInvoker = _hostContext.CreateService()) + { + 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); + } + + _trace.Info($"REPL command exited with code {exitCode}"); + + // 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}.", + Type = exitCode == 0 ? "string" : "error", + VariablesReference = 0 + }; + } + finally + { + // Clean up temp script file + try { File.Delete(scriptFilePath); } + catch { /* best effort */ } + } + } + + /// + /// Expands ${{ }} expressions in the input string using the + /// runner's template evaluator — the same evaluation path that processes + /// step inputs before runs them. + /// + /// Each ${{ expr }} occurrence is individually evaluated and + /// replaced with its masked string result, mirroring the semantics of + /// expression interpolation in a workflow run: step body. + /// + internal string ExpandExpressions(string input, IExecutionContext context) + { + if (string.IsNullOrEmpty(input) || !input.Contains("${{")) + { + return input ?? string.Empty; + } + + var result = new StringBuilder(); + int pos = 0; + + while (pos < input.Length) + { + var start = input.IndexOf("${{", pos, StringComparison.Ordinal); + if (start < 0) + { + result.Append(input, pos, input.Length - pos); + break; + } + + // Append the literal text before the expression + result.Append(input, pos, start - pos); + + var end = input.IndexOf("}}", start + 3, StringComparison.Ordinal); + if (end < 0) + { + // Unterminated expression — keep literal + result.Append(input, start, input.Length - start); + break; + } + + var expr = input.Substring(start + 3, end - start - 3).Trim(); + end += 2; // skip past "}}" + + // Evaluate the expression + try + { + var templateEvaluator = context.ToPipelineTemplateEvaluator(); + var token = new GitHub.DistributedTask.ObjectTemplating.Tokens.BasicExpressionToken( + null, null, null, expr); + var evaluated = templateEvaluator.EvaluateStepDisplayName( + token, + context.ExpressionValues, + context.ExpressionFunctions); + result.Append(_hostContext.SecretMasker.MaskSecrets(evaluated ?? string.Empty)); + } + catch (Exception ex) + { + _trace.Warning($"Expression expansion failed ({ex.GetType().Name})"); + // Keep the original expression literal on failure + result.Append(input, start, end - start); + } + + pos = end; + } + + return result.ToString(); + } + + /// + /// Resolves the default shell the same way + /// does: check job defaults, then fall back to platform default. + /// + internal string ResolveDefaultShell(IExecutionContext context) + { + // Check job defaults + if (context.Global?.JobDefaults != null && + context.Global.JobDefaults.TryGetValue("run", out var runDefaults) && + runDefaults.TryGetValue("shell", out var defaultShell) && + !string.IsNullOrEmpty(defaultShell)) + { + _trace.Info("Using job default shell"); + return defaultShell; + } + +#if OS_WINDOWS + string prependPath = string.Join( + Path.PathSeparator.ToString(), + context.Global?.PrependPath != null ? Enumerable.Reverse(context.Global.PrependPath) : Array.Empty()); + var pwshPath = WhichUtil.Which("pwsh", false, _trace, prependPath); + return !string.IsNullOrEmpty(pwshPath) ? "pwsh" : "powershell"; +#else + return "sh"; +#endif + } + + /// + /// Merges the job context environment with any REPL-specific overrides. + /// + internal Dictionary BuildEnvironment( + IExecutionContext context, + Dictionary replEnv) + { + var env = new Dictionary(VarUtil.EnvironmentVariableKeyComparer); + + // Pull environment from the execution context (same as ActionRunner) + if (context.ExpressionValues.TryGetValue("env", out var envData)) + { + if (envData is DictionaryContextData dictEnv) + { + foreach (var pair in dictEnv) + { + if (pair.Value is StringContextData str) + { + env[pair.Key] = str.Value; + } + } + } + else if (envData is CaseSensitiveDictionaryContextData csEnv) + { + foreach (var pair in csEnv) + { + if (pair.Value is StringContextData str) + { + env[pair.Key] = str.Value; + } + } + } + } + + // Expose runtime context variables to the environment (GITHUB_*, RUNNER_*, etc.) + foreach (var ctxPair in context.ExpressionValues) + { + if (ctxPair.Value is IEnvironmentContextData runtimeContext && runtimeContext != null) + { + foreach (var rtEnv in runtimeContext.GetRuntimeEnvironmentVariables()) + { + env[rtEnv.Key] = rtEnv.Value; + } + } + } + + // Apply REPL-specific overrides last (so they win), + // expanding any ${{ }} expressions in the values + if (replEnv != null) + { + foreach (var pair in replEnv) + { + env[pair.Key] = ExpandExpressions(pair.Value, context); + } + } + + return env; + } + + private void SendOutput(string category, string text) + { + _sendOutput(category, text); + } + + private static EvaluateResponseBody ErrorResult(string message) + { + return new EvaluateResponseBody + { + Result = message, + Type = "error", + VariablesReference = 0 + }; + } + } +} diff --git a/src/Runner.Worker/Dap/DapReplParser.cs b/src/Runner.Worker/Dap/DapReplParser.cs new file mode 100644 index 000000000..c23f4c181 --- /dev/null +++ b/src/Runner.Worker/Dap/DapReplParser.cs @@ -0,0 +1,411 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace GitHub.Runner.Worker.Dap +{ + /// + /// Base type for all REPL DSL commands. + /// + internal abstract class DapReplCommand + { + } + + /// + /// help or help("run") + /// + internal sealed class HelpCommand : DapReplCommand + { + public string Topic { get; set; } + } + + /// + /// run("echo hello") or + /// run("echo hello", shell: "bash", env: { FOO: "bar" }, working_directory: "/tmp") + /// + internal sealed class RunCommand : DapReplCommand + { + public string Script { get; set; } + public string Shell { get; set; } + public Dictionary Env { get; set; } + public string WorkingDirectory { get; set; } + } + + /// + /// Parses REPL input into typed objects. + /// + /// Grammar (intentionally minimal — extend as the DSL grows): + /// + /// help → HelpCommand { Topic = null } + /// help("run") → HelpCommand { Topic = "run" } + /// run("script body") → RunCommand { Script = "script body" } + /// run("script", shell: "bash") → RunCommand { Shell = "bash" } + /// run("script", env: { K: "V" }) → RunCommand { Env = { K → V } } + /// run("script", working_directory: "p")→ RunCommand { WorkingDirectory = "p" } + /// + /// + /// Parsing is intentionally hand-rolled rather than regex-based so it can + /// handle nested braces, quoted strings with escapes, and grow to support + /// future commands without accumulating regex complexity. + /// + internal static class DapReplParser + { + /// + /// Attempts to parse REPL input into a command. Returns null if the + /// input does not match any known DSL command (i.e. it should be + /// treated as an expression instead). + /// + internal static DapReplCommand TryParse(string input, out string error) + { + error = null; + if (string.IsNullOrWhiteSpace(input)) + { + return null; + } + + var trimmed = input.Trim(); + + // help / help("topic") + if (trimmed.Equals("help", StringComparison.OrdinalIgnoreCase) || + trimmed.StartsWith("help(", StringComparison.OrdinalIgnoreCase)) + { + return ParseHelp(trimmed, out error); + } + + // run("...") + if (trimmed.StartsWith("run(", StringComparison.OrdinalIgnoreCase)) + { + return ParseRun(trimmed, out error); + } + + // Not a DSL command + return null; + } + + internal static string GetGeneralHelp() + { + return """ + Actions Debug Console + + Commands: + help Show this help + help("run") Show help for the run command + run("script") Execute a script (like a workflow run step) + + Anything else is evaluated as a GitHub Actions expression. + Example: github.repository + Example: ${{ github.event_name }} + + """; + } + + internal static string GetRunHelp() + { + return """ + run command — execute a script in the job context + + Usage: + run("echo hello") + run("echo $FOO", shell: "bash") + run("echo $FOO", env: { FOO: "bar" }) + run("ls", working_directory: "/tmp") + run("echo $X", shell: "bash", env: { X: "1" }, working_directory: "/tmp") + + Options: + shell: Shell to use (default: job default, e.g. bash) + env: Extra environment variables as { KEY: "value" } + working_directory: Working directory for the command + + Behavior: + - Equivalent to a workflow `run:` step + - Expressions in the script body are expanded (${{ ... }}) + - Output is streamed in real time and secrets are masked + + """; + } + + #region Parsers + + private static HelpCommand ParseHelp(string input, out string error) + { + error = null; + if (input.Equals("help", StringComparison.OrdinalIgnoreCase)) + { + return new HelpCommand(); + } + + // help("topic") + var inner = ExtractParenthesizedArgs(input, "help", out error); + if (error != null) return null; + + var topic = ExtractQuotedString(inner.Trim(), out error); + if (error != null) return null; + + return new HelpCommand { Topic = topic }; + } + + private static RunCommand ParseRun(string input, out string error) + { + error = null; + + var inner = ExtractParenthesizedArgs(input, "run", out error); + if (error != null) return null; + + // Split into argument list respecting quotes and braces + var args = SplitArguments(inner, out error); + if (error != null) return null; + if (args.Count == 0) + { + error = "run() requires a script argument. Example: run(\"echo hello\")"; + return null; + } + + // First arg must be the script body (a quoted string) + var script = ExtractQuotedString(args[0].Trim(), out error); + if (error != null) + { + error = $"First argument to run() must be a quoted string. {error}"; + return null; + } + + var cmd = new RunCommand { Script = script }; + + // Parse remaining keyword arguments + for (int i = 1; i < args.Count; i++) + { + var kv = args[i].Trim(); + var colonIdx = kv.IndexOf(':'); + if (colonIdx <= 0) + { + error = $"Expected keyword argument (e.g. shell: \"bash\"), got: {kv}"; + return null; + } + + var key = kv.Substring(0, colonIdx).Trim(); + var value = kv.Substring(colonIdx + 1).Trim(); + + switch (key.ToLowerInvariant()) + { + case "shell": + cmd.Shell = ExtractQuotedString(value, out error); + if (error != null) { error = $"shell: {error}"; return null; } + break; + + case "working_directory": + cmd.WorkingDirectory = ExtractQuotedString(value, out error); + if (error != null) { error = $"working_directory: {error}"; return null; } + break; + + case "env": + cmd.Env = ParseEnvBlock(value, out error); + if (error != null) { error = $"env: {error}"; return null; } + break; + + default: + error = $"Unknown option: {key}. Valid options: shell, env, working_directory"; + return null; + } + } + + return cmd; + } + + #endregion + + #region Low-level parsing helpers + + /// + /// Given "cmd(...)" returns the inner content between the outer parens. + /// + private static string ExtractParenthesizedArgs(string input, string prefix, out string error) + { + error = null; + var start = prefix.Length; // skip "cmd" + if (start >= input.Length || input[start] != '(') + { + error = $"Expected '(' after {prefix}"; + return null; + } + + if (input[input.Length - 1] != ')') + { + error = $"Expected ')' at end of {prefix}(...)"; + return null; + } + + return input.Substring(start + 1, input.Length - start - 2); + } + + /// + /// Extracts a double-quoted string value, handling escaped quotes. + /// + internal static string ExtractQuotedString(string input, out string error) + { + error = null; + if (string.IsNullOrEmpty(input)) + { + error = "Expected a quoted string, got empty input"; + return null; + } + + if (input[0] != '"') + { + error = $"Expected a quoted string starting with \", got: {Truncate(input, 40)}"; + return null; + } + + var sb = new StringBuilder(); + for (int i = 1; i < input.Length; i++) + { + if (input[i] == '\\' && i + 1 < input.Length) + { + sb.Append(input[i + 1]); + i++; + } + else if (input[i] == '"') + { + // Check nothing meaningful follows the closing quote + var rest = input.Substring(i + 1).Trim(); + if (rest.Length > 0) + { + error = $"Unexpected content after closing quote: {Truncate(rest, 40)}"; + return null; + } + return sb.ToString(); + } + else + { + sb.Append(input[i]); + } + } + + error = "Unterminated string (missing closing \")"; + return null; + } + + /// + /// Splits a comma-separated argument list, respecting quoted strings + /// and nested braces so that "a, b", env: { K: "V, W" } is + /// correctly split into two arguments. + /// + internal static List SplitArguments(string input, out string error) + { + error = null; + var result = new List(); + var current = new StringBuilder(); + int depth = 0; + bool inQuote = false; + + for (int i = 0; i < input.Length; i++) + { + var ch = input[i]; + + if (ch == '\\' && inQuote && i + 1 < input.Length) + { + current.Append(ch); + current.Append(input[++i]); + continue; + } + + if (ch == '"') + { + inQuote = !inQuote; + current.Append(ch); + continue; + } + + if (!inQuote) + { + if (ch == '{') + { + depth++; + current.Append(ch); + continue; + } + if (ch == '}') + { + depth--; + current.Append(ch); + continue; + } + if (ch == ',' && depth == 0) + { + result.Add(current.ToString()); + current.Clear(); + continue; + } + } + + current.Append(ch); + } + + if (inQuote) + { + error = "Unterminated string in arguments"; + return null; + } + if (depth != 0) + { + error = "Unmatched braces in arguments"; + return null; + } + + if (current.Length > 0) + { + result.Add(current.ToString()); + } + + return result; + } + + /// + /// Parses { KEY: "value", KEY2: "value2" } into a dictionary. + /// + internal static Dictionary ParseEnvBlock(string input, out string error) + { + error = null; + var trimmed = input.Trim(); + if (!trimmed.StartsWith("{") || !trimmed.EndsWith("}")) + { + error = "Expected env block in the form { KEY: \"value\" }"; + return null; + } + + var inner = trimmed.Substring(1, trimmed.Length - 2).Trim(); + if (string.IsNullOrEmpty(inner)) + { + return new Dictionary(); + } + + var pairs = SplitArguments(inner, out error); + if (error != null) return null; + + var result = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var pair in pairs) + { + var colonIdx = pair.IndexOf(':'); + if (colonIdx <= 0) + { + error = $"Expected KEY: \"value\" pair, got: {Truncate(pair.Trim(), 40)}"; + return null; + } + + var key = pair.Substring(0, colonIdx).Trim(); + var val = ExtractQuotedString(pair.Substring(colonIdx + 1).Trim(), out error); + if (error != null) return null; + + result[key] = val; + } + + return result; + } + + private static string Truncate(string value, int maxLength) + { + if (value == null) return "(null)"; + return value.Length <= maxLength ? value : value.Substring(0, maxLength) + "..."; + } + + #endregion + } +} diff --git a/src/Runner.Worker/Dap/DapVariableProvider.cs b/src/Runner.Worker/Dap/DapVariableProvider.cs new file mode 100644 index 000000000..85c11f553 --- /dev/null +++ b/src/Runner.Worker/Dap/DapVariableProvider.cs @@ -0,0 +1,373 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using GitHub.DistributedTask.Logging; +using GitHub.DistributedTask.ObjectTemplating.Tokens; +using GitHub.DistributedTask.Pipelines.ContextData; + +namespace GitHub.Runner.Worker.Dap +{ + /// + /// Maps runner execution context data to DAP scopes and variables. + /// + /// This is the single point where runner context values are materialized + /// for the debugger. All values pass through the runner's existing + /// so the DAP + /// surface never exposes anything beyond what a normal CI log would show. + /// + /// The secrets scope is intentionally opaque: keys are visible but every + /// value is replaced with a constant redaction marker. + /// + /// Designed to be reusable by future DAP features (evaluate, hover, REPL) + /// so that masking policy is never duplicated. + /// + internal sealed class DapVariableProvider + { + // Well-known scope names that map to top-level expression contexts. + // Order matters: the index determines the stable variablesReference ID. + private static readonly string[] _scopeNames = + { + "github", "env", "runner", "job", "steps", + "secrets", "inputs", "vars", "matrix", "needs" + }; + + // Scope references occupy the range [1, ScopeReferenceMax]. + private const int _scopeReferenceBase = 1; + private const int _scopeReferenceMax = 100; + + // Dynamic (nested) variable references start above the scope range. + private const int _dynamicReferenceBase = 101; + + private const string _redactedValue = "***"; + + private readonly ISecretMasker _secretMasker; + + // Maps dynamic variable reference IDs to the backing data and its + // dot-separated path (e.g. "github.event.pull_request"). + private readonly Dictionary _variableReferences = new(); + private int _nextVariableReference = _dynamicReferenceBase; + + public DapVariableProvider(ISecretMasker secretMasker) + { + _secretMasker = secretMasker ?? throw new ArgumentNullException(nameof(secretMasker)); + } + + /// + /// Clears all dynamic variable references. + /// Call this whenever the paused execution context changes (e.g. new step) + /// so that stale nested references are not served to the client. + /// + public void Reset() + { + _variableReferences.Clear(); + _nextVariableReference = _dynamicReferenceBase; + } + + /// + /// Returns the list of DAP scopes for the given execution context. + /// Each scope corresponds to a well-known runner expression context + /// (github, env, secrets, …) and carries a stable variablesReference + /// that the client can use to drill into variables. + /// + public List GetScopes(IExecutionContext context) + { + var scopes = new List(); + + if (context?.ExpressionValues == null) + { + return scopes; + } + + for (int i = 0; i < _scopeNames.Length; i++) + { + var scopeName = _scopeNames[i]; + if (!context.ExpressionValues.TryGetValue(scopeName, out var value) || value == null) + { + continue; + } + + var scope = new Scope + { + Name = scopeName, + VariablesReference = _scopeReferenceBase + i, + Expensive = false, + PresentationHint = scopeName == "secrets" ? "registers" : null + }; + + if (value is DictionaryContextData dict) + { + scope.NamedVariables = dict.Count; + } + else if (value is CaseSensitiveDictionaryContextData csDict) + { + scope.NamedVariables = csDict.Count; + } + + scopes.Add(scope); + } + + return scopes; + } + + /// + /// Returns the child variables for a given variablesReference. + /// The reference may point at a top-level scope (1–100) or a + /// dynamically registered nested container (101+). + /// + public List GetVariables(IExecutionContext context, int variablesReference) + { + var variables = new List(); + + if (context?.ExpressionValues == null) + { + return variables; + } + + PipelineContextData data = null; + string basePath = null; + bool isSecretsScope = false; + + if (variablesReference >= _scopeReferenceBase && variablesReference <= _scopeReferenceMax) + { + var scopeIndex = variablesReference - _scopeReferenceBase; + if (scopeIndex < _scopeNames.Length) + { + var scopeName = _scopeNames[scopeIndex]; + isSecretsScope = scopeName == "secrets"; + if (context.ExpressionValues.TryGetValue(scopeName, out data)) + { + basePath = scopeName; + } + } + } + else if (_variableReferences.TryGetValue(variablesReference, out var refData)) + { + data = refData.Data; + basePath = refData.Path; + isSecretsScope = basePath?.StartsWith("secrets", StringComparison.OrdinalIgnoreCase) == true; + } + + if (data == null) + { + return variables; + } + + ConvertToVariables(data, basePath, isSecretsScope, variables); + return variables; + } + + /// + /// Evaluates a GitHub Actions expression (e.g. "github.repository", + /// "${{ github.event_name }}") in the context of the current step and + /// returns a masked result suitable for the DAP evaluate response. + /// + /// Uses the runner's standard + /// so the full expression language is available (functions, operators, + /// context access). + /// + public EvaluateResponseBody EvaluateExpression(string expression, IExecutionContext context) + { + if (context?.ExpressionValues == null) + { + return new EvaluateResponseBody + { + Result = "(no execution context available)", + Type = "string", + VariablesReference = 0 + }; + } + + // Strip ${{ }} wrapper if present + var expr = expression?.Trim() ?? string.Empty; + if (expr.StartsWith("${{") && expr.EndsWith("}}")) + { + expr = expr.Substring(3, expr.Length - 5).Trim(); + } + + if (string.IsNullOrEmpty(expr)) + { + return new EvaluateResponseBody + { + Result = string.Empty, + Type = "string", + VariablesReference = 0 + }; + } + + try + { + var templateEvaluator = context.ToPipelineTemplateEvaluator(); + var token = new BasicExpressionToken(null, null, null, expr); + + var result = templateEvaluator.EvaluateStepDisplayName( + token, + context.ExpressionValues, + context.ExpressionFunctions); + + result = _secretMasker.MaskSecrets(result ?? "null"); + + return new EvaluateResponseBody + { + Result = result, + Type = InferResultType(result), + VariablesReference = 0 + }; + } + catch (Exception ex) + { + var errorMessage = _secretMasker.MaskSecrets($"Evaluation error: {ex.Message}"); + return new EvaluateResponseBody + { + Result = errorMessage, + Type = "string", + VariablesReference = 0 + }; + } + } + + /// + /// Infers a simple DAP type hint from the string representation of a result. + /// + internal static string InferResultType(string value) + { + value = value?.ToLower(); + if (value == null || value == "null") + return "null"; + if (value == "true" || value == "false") + return "boolean"; + if (double.TryParse(value, NumberStyles.Any, + CultureInfo.InvariantCulture, out _)) + return "number"; + if (value.StartsWith("{") || value.StartsWith("[")) + return "object"; + return "string"; + } + + #region Private helpers + + private void ConvertToVariables( + PipelineContextData data, + string basePath, + bool isSecretsScope, + List variables) + { + switch (data) + { + case DictionaryContextData dict: + foreach (var pair in dict) + { + variables.Add(CreateVariable(pair.Key, pair.Value, basePath, isSecretsScope)); + } + break; + + case CaseSensitiveDictionaryContextData csDict: + foreach (var pair in csDict) + { + variables.Add(CreateVariable(pair.Key, pair.Value, basePath, isSecretsScope)); + } + break; + + case ArrayContextData array: + for (int i = 0; i < array.Count; i++) + { + var variable = CreateVariable($"[{i}]", array[i], basePath, isSecretsScope); + variables.Add(variable); + } + break; + } + } + + private Variable CreateVariable( + string name, + PipelineContextData value, + string basePath, + bool isSecretsScope) + { + var childPath = string.IsNullOrEmpty(basePath) ? name : $"{basePath}.{name}"; + var variable = new Variable + { + Name = name, + EvaluateName = $"${{{{ {childPath} }}}}" + }; + + // Secrets scope: redact ALL values regardless of underlying type. + // Keys are visible but values are always replaced with the + // redaction marker, and nested containers are not drillable. + if (isSecretsScope) + { + variable.Value = _redactedValue; + variable.Type = "string"; + variable.VariablesReference = 0; + return variable; + } + + if (value == null) + { + variable.Value = "null"; + variable.Type = "null"; + variable.VariablesReference = 0; + return variable; + } + + switch (value) + { + case StringContextData str: + variable.Value = _secretMasker.MaskSecrets(str.Value); + variable.Type = "string"; + variable.VariablesReference = 0; + break; + + case NumberContextData num: + variable.Value = _secretMasker.MaskSecrets(num.Value.ToString("G15", CultureInfo.InvariantCulture)); + variable.Type = "number"; + variable.VariablesReference = 0; + break; + + case BooleanContextData boolVal: + variable.Value = boolVal.Value ? "true" : "false"; + variable.Type = "boolean"; + variable.VariablesReference = 0; + break; + + case DictionaryContextData dict: + variable.Value = $"Object ({dict.Count} properties)"; + variable.Type = "object"; + variable.VariablesReference = RegisterVariableReference(dict, childPath); + variable.NamedVariables = dict.Count; + break; + + case CaseSensitiveDictionaryContextData csDict: + variable.Value = $"Object ({csDict.Count} properties)"; + variable.Type = "object"; + variable.VariablesReference = RegisterVariableReference(csDict, childPath); + variable.NamedVariables = csDict.Count; + break; + + case ArrayContextData array: + variable.Value = $"Array ({array.Count} items)"; + variable.Type = "array"; + variable.VariablesReference = RegisterVariableReference(array, childPath); + variable.IndexedVariables = array.Count; + break; + + default: + var rawValue = value.ToJToken()?.ToString() ?? "unknown"; + variable.Value = _secretMasker.MaskSecrets(rawValue); + variable.Type = value.GetType().Name; + variable.VariablesReference = 0; + break; + } + + return variable; + } + + private int RegisterVariableReference(PipelineContextData data, string path) + { + var reference = _nextVariableReference++; + _variableReferences[reference] = (data, path); + return reference; + } + + #endregion + } +} diff --git a/src/Runner.Worker/Dap/IDapDebugger.cs b/src/Runner.Worker/Dap/IDapDebugger.cs new file mode 100644 index 000000000..533626a42 --- /dev/null +++ b/src/Runner.Worker/Dap/IDapDebugger.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; +using GitHub.Runner.Common; + +namespace GitHub.Runner.Worker.Dap +{ + public enum DapSessionState + { + NotStarted, + WaitingForConnection, + Initializing, + Ready, + Paused, + Running, + Terminated + } + + [ServiceLocator(Default = typeof(DapDebugger))] + public interface IDapDebugger : IRunnerService + { + Task StartAsync(IExecutionContext jobContext); + Task WaitUntilReadyAsync(); + Task OnStepStartingAsync(IStep step); + void OnStepCompleted(IStep step); + Task OnJobCompletedAsync(); + } +} diff --git a/src/Runner.Worker/ExecutionContext.cs b/src/Runner.Worker/ExecutionContext.cs index d835682a4..70f2a47af 100644 --- a/src/Runner.Worker/ExecutionContext.cs +++ b/src/Runner.Worker/ExecutionContext.cs @@ -969,6 +969,9 @@ namespace GitHub.Runner.Worker // Verbosity (from GitHub.Step_Debug). Global.WriteDebug = Global.Variables.Step_Debug ?? false; + // Debugger enabled flag (from acquire response). + Global.EnableDebugger = message.EnableDebugger; + // Hook up JobServerQueueThrottling event, we will log warning on server tarpit. _jobServerQueue.JobServerQueueThrottling += JobServerQueueThrottling_EventReceived; } diff --git a/src/Runner.Worker/GlobalContext.cs b/src/Runner.Worker/GlobalContext.cs index 64b33c755..60b4ef1fe 100644 --- a/src/Runner.Worker/GlobalContext.cs +++ b/src/Runner.Worker/GlobalContext.cs @@ -27,6 +27,7 @@ namespace GitHub.Runner.Worker public StepsContext StepsContext { get; set; } public Variables Variables { get; set; } public bool WriteDebug { get; set; } + public bool EnableDebugger { get; set; } public string InfrastructureFailureCategory { get; set; } public JObject ContainerHookState { get; set; } public bool HasTemplateEvaluatorMismatch { get; set; } diff --git a/src/Runner.Worker/InternalsVisibleTo.cs b/src/Runner.Worker/InternalsVisibleTo.cs index a825116a6..de556bce3 100644 --- a/src/Runner.Worker/InternalsVisibleTo.cs +++ b/src/Runner.Worker/InternalsVisibleTo.cs @@ -1,3 +1,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Test")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/Runner.Worker/JobRunner.cs b/src/Runner.Worker/JobRunner.cs index 72ee5a403..10623bbef 100644 --- a/src/Runner.Worker/JobRunner.cs +++ b/src/Runner.Worker/JobRunner.cs @@ -13,6 +13,7 @@ using GitHub.DistributedTask.WebApi; using GitHub.Runner.Common; using GitHub.Runner.Common.Util; using GitHub.Runner.Sdk; +using GitHub.Runner.Worker.Dap; using GitHub.Services.Common; using GitHub.Services.WebApi; using Sdk.RSWebApi.Contracts; @@ -28,6 +29,7 @@ namespace GitHub.Runner.Worker public sealed class JobRunner : RunnerService, IJobRunner { + private const string DebuggerConnectionTelemetryPrefix = "DebuggerConnectionResult"; private IJobServerQueue _jobServerQueue; private RunnerSettings _runnerSettings; private ITempDirectoryManager _tempDirectoryManager; @@ -112,6 +114,7 @@ namespace GitHub.Runner.Worker IExecutionContext jobContext = null; CancellationTokenRegistration? runnerShutdownRegistration = null; + IDapDebugger dapDebugger = null; try { // Create the job execution context. @@ -178,6 +181,26 @@ namespace GitHub.Runner.Worker _tempDirectoryManager = HostContext.GetService(); _tempDirectoryManager.InitializeTempDirectory(jobContext); + // Setup the debugger + if (jobContext.Global.EnableDebugger) + { + Trace.Info("Debugger enabled for this job run"); + + try + { + dapDebugger = HostContext.GetService(); + await dapDebugger.StartAsync(jobContext); + } + catch (Exception ex) + { + Trace.Error($"Failed to start DAP debugger: {ex.Message}"); + AddDebuggerConnectionTelemetry(jobContext, $"Failed: {ex.Message}"); + jobContext.Error("Failed to start debugger."); + return await CompleteJobAsync(server, jobContext, message, TaskResult.Failed); + } + } + + // Get the job extension. Trace.Info("Getting job extension."); IJobExtension jobExtension = HostContext.CreateService(); @@ -219,6 +242,33 @@ namespace GitHub.Runner.Worker await Task.WhenAny(_jobServerQueue.JobRecordUpdated.Task, Task.Delay(1000)); } + // Wait for DAP debugger client connection and handshake after "Set up job" + // so the job page shows the setup step before we block on the debugger + if (dapDebugger != null) + { + try + { + await dapDebugger.WaitUntilReadyAsync(); + AddDebuggerConnectionTelemetry(jobContext, "Connected"); + } + catch (OperationCanceledException) when (jobRequestCancellationToken.IsCancellationRequested) + { + Trace.Info("Job was cancelled before debugger client connected."); + AddDebuggerConnectionTelemetry(jobContext, "Canceled"); + jobContext.Error("Job was cancelled before debugger client connected."); + return await CompleteJobAsync(server, jobContext, message, TaskResult.Canceled); + } + catch (Exception ex) + { + Trace.Error($"DAP debugger failed to become ready: {ex.Message}"); + AddDebuggerConnectionTelemetry(jobContext, $"Failed: {ex.Message}"); + + // If debugging was requested but the debugger is not available, fail the job + jobContext.Error("The debugger failed to start or no debugger client connected in time."); + return await CompleteJobAsync(server, jobContext, message, TaskResult.Failed); + } + } + // Run all job steps Trace.Info("Run all job steps."); var stepsRunner = HostContext.GetService(); @@ -259,6 +309,11 @@ namespace GitHub.Runner.Worker runnerShutdownRegistration = null; } + if (dapDebugger != null) + { + await dapDebugger.OnJobCompletedAsync(); + } + await ShutdownQueue(throwOnFailure: false); } } @@ -440,6 +495,15 @@ namespace GitHub.Runner.Worker throw new AggregateException(exceptions); } + private static void AddDebuggerConnectionTelemetry(IExecutionContext jobContext, string result) + { + jobContext.Global.JobTelemetry.Add(new JobTelemetry + { + Type = JobTelemetryType.General, + Message = $"{DebuggerConnectionTelemetryPrefix}: {result}" + }); + } + private void MaskTelemetrySecrets(List jobTelemetry) { foreach (var telemetryItem in jobTelemetry) diff --git a/src/Runner.Worker/StepsRunner.cs b/src/Runner.Worker/StepsRunner.cs index 83ce87f64..21bdfa6f7 100644 --- a/src/Runner.Worker/StepsRunner.cs +++ b/src/Runner.Worker/StepsRunner.cs @@ -10,6 +10,7 @@ using GitHub.DistributedTask.WebApi; using GitHub.Runner.Common; using GitHub.Runner.Common.Util; using GitHub.Runner.Sdk; +using GitHub.Runner.Worker.Dap; using GitHub.Runner.Worker.Expressions; namespace GitHub.Runner.Worker @@ -50,6 +51,7 @@ namespace GitHub.Runner.Worker jobContext.JobContext.Status = (jobContext.Result ?? TaskResult.Succeeded).ToActionResult(); var scopeInputs = new Dictionary(StringComparer.OrdinalIgnoreCase); bool checkPostJobActions = false; + var dapDebugger = HostContext.GetService(); while (jobContext.JobSteps.Count > 0 || !checkPostJobActions) { if (jobContext.JobSteps.Count == 0 && !checkPostJobActions) @@ -226,9 +228,14 @@ namespace GitHub.Runner.Worker } else { + // Pause for DAP debugger before step execution + await dapDebugger?.OnStepStartingAsync(step); + // Run the step await RunStepAsync(step, jobContext.CancellationToken); CompleteStep(step); + + dapDebugger?.OnStepCompleted(step); } } finally @@ -255,6 +262,7 @@ namespace GitHub.Runner.Worker Trace.Info($"Current state: job state = '{jobContext.Result}'"); } + } private async Task RunStepAsync(IStep step, CancellationToken jobCancellationToken) diff --git a/src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs b/src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs index e6ecbf450..328f62160 100644 --- a/src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs +++ b/src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs @@ -253,6 +253,13 @@ namespace GitHub.DistributedTask.Pipelines set; } + [DataMember(EmitDefaultValue = false)] + public bool EnableDebugger + { + get; + set; + } + /// /// Gets the collection of variables associated with the current context. /// diff --git a/src/Test/L0/Sdk/RSWebApi/AgentJobRequestMessageL0.cs b/src/Test/L0/Sdk/RSWebApi/AgentJobRequestMessageL0.cs new file mode 100644 index 000000000..33b30d308 --- /dev/null +++ b/src/Test/L0/Sdk/RSWebApi/AgentJobRequestMessageL0.cs @@ -0,0 +1,76 @@ +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization.Json; +using System.Text; +using Xunit; +using GitHub.DistributedTask.Pipelines; + +namespace GitHub.Actions.RunService.WebApi.Tests; + +public sealed class AgentJobRequestMessageL0 +{ + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void VerifyEnableDebuggerDeserialization_WithTrue() + { + // 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"); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void VerifyEnableDebuggerDeserialization_DefaultToFalse() + { + // 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"); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void VerifyEnableDebuggerDeserialization_WithFalse() + { + // 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"); + } + + private static string DoubleQuotify(string text) + { + return text.Replace('\'', '"'); + } +} diff --git a/src/Test/L0/ServiceInterfacesL0.cs b/src/Test/L0/ServiceInterfacesL0.cs index 59b890285..ec8a270f5 100644 --- a/src/Test/L0/ServiceInterfacesL0.cs +++ b/src/Test/L0/ServiceInterfacesL0.cs @@ -2,6 +2,7 @@ using GitHub.Runner.Listener.Check; using GitHub.Runner.Listener.Configuration; using GitHub.Runner.Worker; +using GitHub.Runner.Worker.Dap; using GitHub.Runner.Worker.Container.ContainerHooks; using GitHub.Runner.Worker.Handlers; using System; diff --git a/src/Test/L0/Worker/DapDebuggerL0.cs b/src/Test/L0/Worker/DapDebuggerL0.cs new file mode 100644 index 000000000..f2c8557d1 --- /dev/null +++ b/src/Test/L0/Worker/DapDebuggerL0.cs @@ -0,0 +1,616 @@ +using System; +using System.IO; +using System.Net; +using System.Net.Sockets; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Moq; +using GitHub.Runner.Worker; +using GitHub.Runner.Worker.Dap; +using Newtonsoft.Json; +using Xunit; + +namespace GitHub.Runner.Common.Tests.Worker +{ + public sealed class DapDebuggerL0 + { + private const string PortEnvironmentVariable = "ACTIONS_RUNNER_DAP_PORT"; + private const string TimeoutEnvironmentVariable = "ACTIONS_RUNNER_DAP_CONNECTION_TIMEOUT"; + private DapDebugger _debugger; + + private TestHostContext CreateTestContext([CallerMemberName] string testName = "") + { + var hc = new TestHostContext(this, testName); + _debugger = new DapDebugger(); + _debugger.Initialize(hc); + return hc; + } + + private static async Task WithEnvironmentVariableAsync(string name, string value, Func action) + { + var originalValue = Environment.GetEnvironmentVariable(name); + Environment.SetEnvironmentVariable(name, value); + try + { + await action(); + } + finally + { + Environment.SetEnvironmentVariable(name, originalValue); + } + } + + private static void WithEnvironmentVariable(string name, string value, Action action) + { + var originalValue = Environment.GetEnvironmentVariable(name); + Environment.SetEnvironmentVariable(name, value); + try + { + action(); + } + finally + { + Environment.SetEnvironmentVariable(name, originalValue); + } + } + + private static int GetFreePort() + { + using var listener = new TcpListener(IPAddress.Loopback, 0); + listener.Start(); + return ((IPEndPoint)listener.LocalEndpoint).Port; + } + + private static async Task ConnectClientAsync(int port) + { + var client = new TcpClient(); + await client.ConnectAsync(IPAddress.Loopback, port); + return client; + } + + private static async Task SendRequestAsync(NetworkStream stream, Request request) + { + var json = JsonConvert.SerializeObject(request); + var body = Encoding.UTF8.GetBytes(json); + var header = $"Content-Length: {body.Length}\r\n\r\n"; + var headerBytes = Encoding.ASCII.GetBytes(header); + + await stream.WriteAsync(headerBytes, 0, headerBytes.Length); + await stream.WriteAsync(body, 0, body.Length); + await stream.FlushAsync(); + } + + /// + /// Reads a single DAP-framed message from a stream with a timeout. + /// Parses the Content-Length header, reads exactly that many bytes, + /// and returns the JSON body. Fails with a clear error on timeout. + /// + private static async Task ReadDapMessageAsync(NetworkStream stream, TimeSpan timeout) + { + using var cts = new CancellationTokenSource(timeout); + var token = cts.Token; + + var headerBuilder = new StringBuilder(); + var buffer = new byte[1]; + var contentLength = -1; + + while (true) + { + var readTask = stream.ReadAsync(buffer, 0, 1, token); + var bytesRead = await readTask; + if (bytesRead == 0) + { + throw new EndOfStreamException("Connection closed while reading DAP headers"); + } + + headerBuilder.Append((char)buffer[0]); + var headers = headerBuilder.ToString(); + if (headers.EndsWith("\r\n\r\n", StringComparison.Ordinal)) + { + foreach (var line in headers.Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries)) + { + if (line.StartsWith("Content-Length: ", StringComparison.OrdinalIgnoreCase)) + { + contentLength = int.Parse(line.Substring("Content-Length: ".Length).Trim()); + } + } + break; + } + } + + if (contentLength < 0) + { + throw new InvalidOperationException("No Content-Length header found in DAP message"); + } + + var body = new byte[contentLength]; + var totalRead = 0; + while (totalRead < contentLength) + { + var bytesRead = await stream.ReadAsync(body, totalRead, contentLength - totalRead, token); + if (bytesRead == 0) + { + throw new EndOfStreamException("Connection closed while reading DAP body"); + } + totalRead += bytesRead; + } + + return Encoding.UTF8.GetString(body); + } + + private static Mock CreateJobContext(CancellationToken cancellationToken, string jobName = null) + { + var jobContext = new Mock(); + jobContext.Setup(x => x.CancellationToken).Returns(cancellationToken); + jobContext + .Setup(x => x.GetGitHubContext(It.IsAny())) + .Returns((string contextName) => string.Equals(contextName, "job", StringComparison.Ordinal) ? jobName : null); + return jobContext; + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void InitializeSucceeds() + { + using (CreateTestContext()) + { + Assert.NotNull(_debugger); + Assert.False(_debugger.IsActive); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ResolvePortUsesCustomPortFromEnvironment() + { + using (CreateTestContext()) + { + WithEnvironmentVariable(PortEnvironmentVariable, "9999", () => + { + Assert.Equal(9999, _debugger.ResolvePort()); + }); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ResolvePortIgnoresInvalidPortFromEnvironment() + { + using (CreateTestContext()) + { + WithEnvironmentVariable(PortEnvironmentVariable, "not-a-number", () => + { + Assert.Equal(4711, _debugger.ResolvePort()); + }); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ResolvePortIgnoresOutOfRangePortFromEnvironment() + { + using (CreateTestContext()) + { + WithEnvironmentVariable(PortEnvironmentVariable, "99999", () => + { + Assert.Equal(4711, _debugger.ResolvePort()); + }); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ResolveTimeoutUsesCustomTimeoutFromEnvironment() + { + using (CreateTestContext()) + { + WithEnvironmentVariable(TimeoutEnvironmentVariable, "30", () => + { + Assert.Equal(30, _debugger.ResolveTimeout()); + }); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ResolveTimeoutIgnoresInvalidTimeoutFromEnvironment() + { + using (CreateTestContext()) + { + WithEnvironmentVariable(TimeoutEnvironmentVariable, "not-a-number", () => + { + Assert.Equal(15, _debugger.ResolveTimeout()); + }); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ResolveTimeoutIgnoresZeroTimeoutFromEnvironment() + { + using (CreateTestContext()) + { + WithEnvironmentVariable(TimeoutEnvironmentVariable, "0", () => + { + Assert.Equal(15, _debugger.ResolveTimeout()); + }); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task StartAndStopLifecycle() + { + using (CreateTestContext()) + { + var port = GetFreePort(); + await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var jobContext = CreateJobContext(cts.Token); + await _debugger.StartAsync(jobContext.Object); + using var client = await ConnectClientAsync(port); + Assert.True(client.Connected); + await _debugger.StopAsync(); + }); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task StartAndStopMultipleTimesDoesNotThrow() + { + using (CreateTestContext()) + { + foreach (var port in new[] { GetFreePort(), GetFreePort() }) + { + await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var jobContext = CreateJobContext(cts.Token); + await _debugger.StartAsync(jobContext.Object); + await _debugger.StopAsync(); + }); + } + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task WaitUntilReadyCompletesAfterClientConnectionAndConfigurationDone() + { + using (CreateTestContext()) + { + var port = GetFreePort(); + await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var jobContext = CreateJobContext(cts.Token); + await _debugger.StartAsync(jobContext.Object); + + var waitTask = _debugger.WaitUntilReadyAsync(); + using var client = await ConnectClientAsync(port); + await SendRequestAsync(client.GetStream(), new Request + { + Seq = 1, + Type = "request", + Command = "configurationDone" + }); + + await waitTask; + Assert.Equal(DapSessionState.Ready, _debugger.State); + await _debugger.StopAsync(); + }); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task StartStoresJobContextForThreadsRequest() + { + using (CreateTestContext()) + { + var port = GetFreePort(); + await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var jobContext = CreateJobContext(cts.Token, "ci-job"); + await _debugger.StartAsync(jobContext.Object); + using var client = await ConnectClientAsync(port); + var stream = client.GetStream(); + await SendRequestAsync(client.GetStream(), new Request + { + Seq = 1, + Type = "request", + Command = "threads" + }); + + var response = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)); + Assert.Contains("\"command\":\"threads\"", response); + Assert.Contains("\"name\":\"Job: ci-job\"", response); + await _debugger.StopAsync(); + }); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task CancellationUnblocksAndOnJobCompletedTerminates() + { + using (CreateTestContext()) + { + var port = GetFreePort(); + await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var jobContext = CreateJobContext(cts.Token); + await _debugger.StartAsync(jobContext.Object); + + var waitTask = _debugger.WaitUntilReadyAsync(); + using var client = await ConnectClientAsync(port); + await SendRequestAsync(client.GetStream(), new Request + { + Seq = 1, + Type = "request", + Command = "configurationDone" + }); + + await waitTask; + cts.Cancel(); + + // In the real runner, JobRunner always calls OnJobCompletedAsync + // from a finally block. The cancellation callback only unblocks + // pending waits; OnJobCompletedAsync handles state + cleanup. + await _debugger.OnJobCompletedAsync(); + Assert.Equal(DapSessionState.Terminated, _debugger.State); + }); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task StopWithoutStartDoesNotThrow() + { + using (CreateTestContext()) + { + await _debugger.StopAsync(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task OnJobCompletedTerminatesSession() + { + using (CreateTestContext()) + { + var port = GetFreePort(); + await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var jobContext = CreateJobContext(cts.Token); + await _debugger.StartAsync(jobContext.Object); + + var waitTask = _debugger.WaitUntilReadyAsync(); + using var client = await ConnectClientAsync(port); + await SendRequestAsync(client.GetStream(), new Request + { + Seq = 1, + Type = "request", + Command = "configurationDone" + }); + + await waitTask; + await _debugger.OnJobCompletedAsync(); + Assert.Equal(DapSessionState.Terminated, _debugger.State); + }); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task WaitUntilReadyBeforeStartIsNoOp() + { + using (CreateTestContext()) + { + await _debugger.WaitUntilReadyAsync(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task WaitUntilReadyJobCancellationPropagatesAsOperationCancelledException() + { + using (CreateTestContext()) + { + var port = GetFreePort(); + await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var jobContext = CreateJobContext(cts.Token); + await _debugger.StartAsync(jobContext.Object); + + var waitTask = _debugger.WaitUntilReadyAsync(); + await Task.Delay(50); + cts.Cancel(); + + var ex = await Assert.ThrowsAnyAsync(() => waitTask); + Assert.IsNotType(ex); + await _debugger.StopAsync(); + }); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task InitializeRequestOverSocketPreservesProtocolMetadataWhenSecretsCollide() + { + using (var hc = CreateTestContext()) + { + hc.SecretMasker.AddValue("response"); + hc.SecretMasker.AddValue("initialize"); + hc.SecretMasker.AddValue("event"); + hc.SecretMasker.AddValue("initialized"); + + var port = GetFreePort(); + await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var jobContext = CreateJobContext(cts.Token); + 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 = "initialize" + }); + + var response = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)); + Assert.Contains("\"type\":\"response\"", response); + Assert.Contains("\"command\":\"initialize\"", response); + Assert.Contains("\"success\":true", response); + + var initializedEvent = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)); + Assert.Contains("\"type\":\"event\"", initializedEvent); + Assert.Contains("\"event\":\"initialized\"", initializedEvent); + + await _debugger.StopAsync(); + }); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task CancellationDuringStepPauseReleasesWait() + { + using (CreateTestContext()) + { + var port = GetFreePort(); + await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var jobContext = CreateJobContext(cts.Token); + await _debugger.StartAsync(jobContext.Object); + + // Complete handshake so session is ready + 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 waitTask; + + // Simulate a step starting (which pauses) + var step = new Mock(); + step.Setup(s => s.DisplayName).Returns("Test Step"); + step.Setup(s => s.ExecutionContext).Returns((IExecutionContext)null); + var stepTask = _debugger.OnStepStartingAsync(step.Object); + + // Give the step time to pause + await Task.Delay(50); + + // Cancel the job — should release the step pause + cts.Cancel(); + await stepTask; + + // In the real runner, OnJobCompletedAsync always follows. + await _debugger.OnJobCompletedAsync(); + Assert.Equal(DapSessionState.Terminated, _debugger.State); + }); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task StopAsyncSafeAtAnyLifecyclePoint() + { + using (CreateTestContext()) + { + // StopAsync before start + await _debugger.StopAsync(); + + // Start then immediate stop (no connection, no WaitUntilReady) + var port = GetFreePort(); + await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var jobContext = CreateJobContext(cts.Token); + await _debugger.StartAsync(jobContext.Object); + await _debugger.StopAsync(); + }); + + // StopAsync after already stopped + await _debugger.StopAsync(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task OnJobCompletedSendsTerminatedAndExitedEvents() + { + using (CreateTestContext()) + { + var port = GetFreePort(); + await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var jobContext = CreateJobContext(cts.Token); + 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" + }); + + // Read the configurationDone response + await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)); + await waitTask; + + // Complete the job — events are sent via OnJobCompletedAsync + await _debugger.OnJobCompletedAsync(); + + var msg1 = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)); + var msg2 = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)); + + // Both events should arrive (order may vary) + var combined = msg1 + msg2; + Assert.Contains("\"event\":\"terminated\"", combined); + Assert.Contains("\"event\":\"exited\"", combined); + }); + } + } + } +} diff --git a/src/Test/L0/Worker/DapMessagesL0.cs b/src/Test/L0/Worker/DapMessagesL0.cs new file mode 100644 index 000000000..1b8285717 --- /dev/null +++ b/src/Test/L0/Worker/DapMessagesL0.cs @@ -0,0 +1,233 @@ +using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Xunit; +using GitHub.Runner.Worker.Dap; + +namespace GitHub.Runner.Common.Tests.Worker +{ + public sealed class DapMessagesL0 + { + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void RequestSerializesCorrectly() + { + var request = new Request + { + Seq = 1, + Type = "request", + Command = "initialize", + Arguments = JObject.FromObject(new { clientID = "test-client" }) + }; + + var json = JsonConvert.SerializeObject(request); + var deserialized = JsonConvert.DeserializeObject(json); + + Assert.Equal(1, deserialized.Seq); + Assert.Equal("request", deserialized.Type); + Assert.Equal("initialize", deserialized.Command); + Assert.Equal("test-client", deserialized.Arguments["clientID"].ToString()); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ResponseSerializesCorrectly() + { + var response = new Response + { + Seq = 2, + Type = "response", + RequestSeq = 1, + Success = true, + Command = "initialize", + Body = new Capabilities { SupportsConfigurationDoneRequest = true } + }; + + var json = JsonConvert.SerializeObject(response); + var deserialized = JsonConvert.DeserializeObject(json); + + Assert.Equal(2, deserialized.Seq); + Assert.Equal("response", deserialized.Type); + Assert.Equal(1, deserialized.RequestSeq); + Assert.True(deserialized.Success); + Assert.Equal("initialize", deserialized.Command); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void EventSerializesWithCorrectType() + { + var evt = new Event + { + EventType = "stopped", + Body = new StoppedEventBody + { + Reason = "entry", + Description = "Stopped at entry", + ThreadId = 1, + AllThreadsStopped = true + } + }; + + Assert.Equal("event", evt.Type); + + var json = JsonConvert.SerializeObject(evt); + Assert.Contains("\"type\":\"event\"", json); + Assert.Contains("\"event\":\"stopped\"", json); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void StoppedEventBodyOmitsNullFields() + { + var body = new StoppedEventBody + { + Reason = "step" + }; + + var json = JsonConvert.SerializeObject(body); + Assert.Contains("\"reason\":\"step\"", json); + Assert.DoesNotContain("\"threadId\"", json); + Assert.DoesNotContain("\"allThreadsStopped\"", json); + Assert.DoesNotContain("\"description\"", json); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void CapabilitiesMvpDefaults() + { + var caps = new Capabilities + { + SupportsConfigurationDoneRequest = true, + SupportsFunctionBreakpoints = false, + SupportsStepBack = false + }; + + var json = JsonConvert.SerializeObject(caps); + var deserialized = JsonConvert.DeserializeObject(json); + + Assert.True(deserialized.SupportsConfigurationDoneRequest); + Assert.False(deserialized.SupportsFunctionBreakpoints); + Assert.False(deserialized.SupportsStepBack); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ContinueResponseBodySerialization() + { + var body = new ContinueResponseBody { AllThreadsContinued = true }; + var json = JsonConvert.SerializeObject(body); + var deserialized = JsonConvert.DeserializeObject(json); + + Assert.True(deserialized.AllThreadsContinued); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ThreadsResponseBodySerialization() + { + var body = new ThreadsResponseBody + { + Threads = new List + { + new Thread { Id = 1, Name = "Job Thread" } + } + }; + + var json = JsonConvert.SerializeObject(body); + var deserialized = JsonConvert.DeserializeObject(json); + + Assert.Single(deserialized.Threads); + Assert.Equal(1, deserialized.Threads[0].Id); + Assert.Equal("Job Thread", deserialized.Threads[0].Name); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void StackFrameSerialization() + { + var frame = new StackFrame + { + Id = 1, + Name = "Step: Checkout", + Line = 1, + Column = 1, + PresentationHint = "normal" + }; + + var json = JsonConvert.SerializeObject(frame); + var deserialized = JsonConvert.DeserializeObject(json); + + Assert.Equal(1, deserialized.Id); + Assert.Equal("Step: Checkout", deserialized.Name); + Assert.Equal("normal", deserialized.PresentationHint); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ExitedEventBodySerialization() + { + var body = new ExitedEventBody { ExitCode = 130 }; + var json = JsonConvert.SerializeObject(body); + var deserialized = JsonConvert.DeserializeObject(json); + + Assert.Equal(130, deserialized.ExitCode); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void DapCommandEnumValues() + { + Assert.Equal(0, (int)DapCommand.Continue); + Assert.Equal(1, (int)DapCommand.Next); + Assert.Equal(4, (int)DapCommand.Disconnect); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void RequestDeserializesFromRawJson() + { + var json = @"{""seq"":5,""type"":""request"",""command"":""continue"",""arguments"":{""threadId"":1}}"; + var request = JsonConvert.DeserializeObject(json); + + Assert.Equal(5, request.Seq); + Assert.Equal("request", request.Type); + Assert.Equal("continue", request.Command); + Assert.Equal(1, request.Arguments["threadId"].Value()); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ErrorResponseBodySerialization() + { + var body = new ErrorResponseBody + { + Error = new Message + { + Id = 1, + Format = "Something went wrong", + ShowUser = true + } + }; + + var json = JsonConvert.SerializeObject(body); + var deserialized = JsonConvert.DeserializeObject(json); + + Assert.Equal(1, deserialized.Error.Id); + Assert.Equal("Something went wrong", deserialized.Error.Format); + Assert.True(deserialized.Error.ShowUser); + } + } +} diff --git a/src/Test/L0/Worker/DapReplExecutorL0.cs b/src/Test/L0/Worker/DapReplExecutorL0.cs new file mode 100644 index 000000000..687d2093a --- /dev/null +++ b/src/Test/L0/Worker/DapReplExecutorL0.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using GitHub.DistributedTask.Expressions2; +using GitHub.DistributedTask.Pipelines.ContextData; +using GitHub.Runner.Common.Tests; +using GitHub.Runner.Worker; +using GitHub.Runner.Worker.Dap; +using Moq; +using Xunit; + +namespace GitHub.Runner.Common.Tests.Worker +{ + public sealed class DapReplExecutorL0 + { + private TestHostContext _hc; + private DapReplExecutor _executor; + private List _sentEvents; + + private TestHostContext CreateTestContext([CallerMemberName] string testName = "") + { + _hc = new TestHostContext(this, testName); + _sentEvents = new List(); + _executor = new DapReplExecutor(_hc, (category, text) => + { + _sentEvents.Add(new Event + { + EventType = "output", + Body = new OutputEventBody + { + Category = category, + Output = text + } + }); + }); + return _hc; + } + + private Mock CreateMockContext( + DictionaryContextData exprValues = null, + IDictionary> jobDefaults = null) + { + var mock = new Mock(); + mock.Setup(x => x.ExpressionValues).Returns(exprValues ?? new DictionaryContextData()); + mock.Setup(x => x.ExpressionFunctions).Returns(new List()); + + var global = new GlobalContext + { + PrependPath = new List(), + JobDefaults = jobDefaults + ?? new Dictionary>(StringComparer.OrdinalIgnoreCase), + }; + mock.Setup(x => x.Global).Returns(global); + + return mock; + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async Task ExecuteRunCommand_NullContext_ReturnsError() + { + using (CreateTestContext()) + { + var command = new RunCommand { Script = "echo hello" }; + var result = await _executor.ExecuteRunCommandAsync(command, null, CancellationToken.None); + + Assert.Equal("error", result.Type); + Assert.Contains("No execution context available", result.Result); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ExpandExpressions_NoExpressions_ReturnsInput() + { + using (CreateTestContext()) + { + var context = CreateMockContext(); + var result = _executor.ExpandExpressions("echo hello", context.Object); + + Assert.Equal("echo hello", result); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ExpandExpressions_NullInput_ReturnsEmpty() + { + using (CreateTestContext()) + { + var context = CreateMockContext(); + var result = _executor.ExpandExpressions(null, context.Object); + + Assert.Equal(string.Empty, result); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ExpandExpressions_EmptyInput_ReturnsEmpty() + { + using (CreateTestContext()) + { + var context = CreateMockContext(); + var result = _executor.ExpandExpressions("", context.Object); + + Assert.Equal(string.Empty, result); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ExpandExpressions_UnterminatedExpression_KeepsLiteral() + { + using (CreateTestContext()) + { + var context = CreateMockContext(); + var result = _executor.ExpandExpressions("echo ${{ github.repo", context.Object); + + Assert.Equal("echo ${{ github.repo", result); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ResolveDefaultShell_NoJobDefaults_ReturnsPlatformDefault() + { + using (CreateTestContext()) + { + var context = CreateMockContext(); + var result = _executor.ResolveDefaultShell(context.Object); + +#if OS_WINDOWS + Assert.True(result == "pwsh" || result == "powershell"); +#else + Assert.Equal("sh", result); +#endif + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ResolveDefaultShell_WithJobDefault_ReturnsJobDefault() + { + using (CreateTestContext()) + { + var jobDefaults = new Dictionary>(StringComparer.OrdinalIgnoreCase) + { + ["run"] = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + ["shell"] = "bash" + } + }; + var context = CreateMockContext(jobDefaults: jobDefaults); + var result = _executor.ResolveDefaultShell(context.Object); + + Assert.Equal("bash", result); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void BuildEnvironment_MergesEnvContextAndReplOverrides() + { + using (CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + var envData = new DictionaryContextData + { + ["FOO"] = new StringContextData("bar"), + }; + exprValues["env"] = envData; + + var context = CreateMockContext(exprValues); + var replEnv = new Dictionary { { "BAZ", "qux" } }; + var result = _executor.BuildEnvironment(context.Object, replEnv); + + Assert.Equal("bar", result["FOO"]); + Assert.Equal("qux", result["BAZ"]); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void BuildEnvironment_ReplOverridesWin() + { + using (CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + var envData = new DictionaryContextData + { + ["FOO"] = new StringContextData("original"), + }; + exprValues["env"] = envData; + + var context = CreateMockContext(exprValues); + var replEnv = new Dictionary { { "FOO", "override" } }; + var result = _executor.BuildEnvironment(context.Object, replEnv); + + Assert.Equal("override", result["FOO"]); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void BuildEnvironment_NullReplEnv_ReturnsContextEnvOnly() + { + using (CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + var envData = new DictionaryContextData + { + ["FOO"] = new StringContextData("bar"), + }; + exprValues["env"] = envData; + + var context = CreateMockContext(exprValues); + var result = _executor.BuildEnvironment(context.Object, null); + + Assert.Equal("bar", result["FOO"]); + Assert.False(result.ContainsKey("BAZ")); + } + } + } +} diff --git a/src/Test/L0/Worker/DapReplParserL0.cs b/src/Test/L0/Worker/DapReplParserL0.cs new file mode 100644 index 000000000..0a15a37f4 --- /dev/null +++ b/src/Test/L0/Worker/DapReplParserL0.cs @@ -0,0 +1,314 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using GitHub.Runner.Common.Tests; +using GitHub.Runner.Worker.Dap; +using Xunit; + +namespace GitHub.Runner.Common.Tests.Worker +{ + public sealed class DapReplParserL0 + { + #region help command + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Parse_HelpReturnsHelpCommand() + { + var cmd = DapReplParser.TryParse("help", out var error); + + Assert.Null(error); + var help = Assert.IsType(cmd); + Assert.Null(help.Topic); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Parse_HelpCaseInsensitive() + { + var cmd = DapReplParser.TryParse("Help", out var error); + Assert.Null(error); + Assert.IsType(cmd); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Parse_HelpWithTopic() + { + var cmd = DapReplParser.TryParse("help(\"run\")", out var error); + + Assert.Null(error); + var help = Assert.IsType(cmd); + Assert.Equal("run", help.Topic); + } + + #endregion + + #region run command — basic + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Parse_RunSimpleScript() + { + var cmd = DapReplParser.TryParse("run(\"echo hello\")", out var error); + + Assert.Null(error); + var run = Assert.IsType(cmd); + Assert.Equal("echo hello", run.Script); + Assert.Null(run.Shell); + Assert.Null(run.Env); + Assert.Null(run.WorkingDirectory); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Parse_RunWithShell() + { + var cmd = DapReplParser.TryParse("run(\"echo hello\", shell: \"bash\")", out var error); + + Assert.Null(error); + var run = Assert.IsType(cmd); + Assert.Equal("echo hello", run.Script); + Assert.Equal("bash", run.Shell); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Parse_RunWithWorkingDirectory() + { + var cmd = DapReplParser.TryParse("run(\"ls\", working_directory: \"/tmp\")", out var error); + + Assert.Null(error); + var run = Assert.IsType(cmd); + Assert.Equal("ls", run.Script); + Assert.Equal("/tmp", run.WorkingDirectory); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Parse_RunWithEnv() + { + var cmd = DapReplParser.TryParse("run(\"echo $FOO\", env: { FOO: \"bar\" })", out var error); + + Assert.Null(error); + var run = Assert.IsType(cmd); + Assert.Equal("echo $FOO", run.Script); + Assert.NotNull(run.Env); + Assert.Equal("bar", run.Env["FOO"]); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Parse_RunWithMultipleEnvVars() + { + var cmd = DapReplParser.TryParse("run(\"echo\", env: { A: \"1\", B: \"2\" })", out var error); + + Assert.Null(error); + var run = Assert.IsType(cmd); + Assert.Equal(2, run.Env.Count); + Assert.Equal("1", run.Env["A"]); + Assert.Equal("2", run.Env["B"]); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Parse_RunWithAllOptions() + { + var input = "run(\"echo $X\", shell: \"zsh\", env: { X: \"1\" }, working_directory: \"/tmp\")"; + var cmd = DapReplParser.TryParse(input, out var error); + + Assert.Null(error); + var run = Assert.IsType(cmd); + Assert.Equal("echo $X", run.Script); + Assert.Equal("zsh", run.Shell); + Assert.Equal("1", run.Env["X"]); + Assert.Equal("/tmp", run.WorkingDirectory); + } + + #endregion + + #region run command — edge cases + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Parse_RunWithEscapedQuotes() + { + var cmd = DapReplParser.TryParse("run(\"echo \\\"hello\\\"\")", out var error); + + Assert.Null(error); + var run = Assert.IsType(cmd); + Assert.Equal("echo \"hello\"", run.Script); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Parse_RunWithCommaInEnvValue() + { + var cmd = DapReplParser.TryParse("run(\"echo\", env: { CSV: \"a,b,c\" })", out var error); + + Assert.Null(error); + var run = Assert.IsType(cmd); + Assert.Equal("a,b,c", run.Env["CSV"]); + } + + #endregion + + #region error cases + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Parse_RunEmptyArgsReturnsError() + { + var cmd = DapReplParser.TryParse("run()", out var error); + + Assert.NotNull(error); + Assert.Null(cmd); + Assert.Contains("requires a script argument", error); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Parse_RunUnquotedArgReturnsError() + { + var cmd = DapReplParser.TryParse("run(echo hello)", out var error); + + Assert.NotNull(error); + Assert.Null(cmd); + Assert.Contains("quoted string", error); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Parse_RunUnknownOptionReturnsError() + { + var cmd = DapReplParser.TryParse("run(\"echo\", timeout: \"10\")", out var error); + + Assert.NotNull(error); + Assert.Null(cmd); + Assert.Contains("Unknown option", error); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Parse_RunMissingClosingParenReturnsError() + { + var cmd = DapReplParser.TryParse("run(\"echo\"", out var error); + + Assert.NotNull(error); + Assert.Null(cmd); + } + + #endregion + + #region non-DSL input falls through + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Parse_ExpressionReturnsNull() + { + var cmd = DapReplParser.TryParse("github.repository", out var error); + + Assert.Null(error); + Assert.Null(cmd); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Parse_WrappedExpressionReturnsNull() + { + var cmd = DapReplParser.TryParse("${{ github.event_name }}", out var error); + + Assert.Null(error); + Assert.Null(cmd); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Parse_EmptyInputReturnsNull() + { + var cmd = DapReplParser.TryParse("", out var error); + Assert.Null(error); + Assert.Null(cmd); + + cmd = DapReplParser.TryParse(null, out error); + Assert.Null(error); + Assert.Null(cmd); + } + + #endregion + + #region help text + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetGeneralHelp_ContainsCommands() + { + var help = DapReplParser.GetGeneralHelp(); + + Assert.Contains("help", help); + Assert.Contains("run", help); + Assert.Contains("expression", help, System.StringComparison.OrdinalIgnoreCase); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetRunHelp_ContainsOptions() + { + var help = DapReplParser.GetRunHelp(); + + Assert.Contains("shell", help); + Assert.Contains("env", help); + Assert.Contains("working_directory", help); + } + + #endregion + + #region internal parser helpers + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void SplitArguments_HandlesNestedBraces() + { + var args = DapReplParser.SplitArguments("\"hello\", env: { A: \"1\", B: \"2\" }", out var error); + + Assert.Null(error); + Assert.Equal(2, args.Count); + Assert.Equal("\"hello\"", args[0].Trim()); + Assert.Contains("A:", args[1]); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ParseEnvBlock_HandlesEmptyBlock() + { + var result = DapReplParser.ParseEnvBlock("{ }", out var error); + + Assert.Null(error); + Assert.NotNull(result); + Assert.Empty(result); + } + + #endregion + } +} diff --git a/src/Test/L0/Worker/DapVariableProviderL0.cs b/src/Test/L0/Worker/DapVariableProviderL0.cs new file mode 100644 index 000000000..401fd098f --- /dev/null +++ b/src/Test/L0/Worker/DapVariableProviderL0.cs @@ -0,0 +1,728 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using GitHub.DistributedTask.Pipelines.ContextData; +using GitHub.DistributedTask.WebApi; +using GitHub.Runner.Common; +using GitHub.Runner.Common.Tests; +using GitHub.Runner.Worker; +using GitHub.Runner.Worker.Dap; +using Xunit; + +namespace GitHub.Runner.Common.Tests.Worker +{ + public sealed class DapVariableProviderL0 + { + private TestHostContext _hc; + private DapVariableProvider _provider; + + private TestHostContext CreateTestContext([CallerMemberName] string testName = "") + { + _hc = new TestHostContext(this, testName); + _provider = new DapVariableProvider(_hc.SecretMasker); + return _hc; + } + + private Moq.Mock CreateMockContext(DictionaryContextData expressionValues) + { + var mock = new Moq.Mock(); + mock.Setup(x => x.ExpressionValues).Returns(expressionValues); + return mock; + } + + #region GetScopes tests + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetScopes_ReturnsEmptyWhenContextIsNull() + { + using (CreateTestContext()) + { + var scopes = _provider.GetScopes(null); + Assert.Empty(scopes); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetScopes_ReturnsOnlyPopulatedScopes() + { + using (CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + exprValues["github"] = new DictionaryContextData + { + { "repository", new StringContextData("owner/repo") } + }; + exprValues["env"] = new DictionaryContextData + { + { "CI", new StringContextData("true") }, + { "HOME", new StringContextData("/home/runner") } + }; + // "runner" is not set — should not appear in scopes + + var ctx = CreateMockContext(exprValues); + var scopes = _provider.GetScopes(ctx.Object); + + Assert.Equal(2, scopes.Count); + Assert.Equal("github", scopes[0].Name); + Assert.Equal("env", scopes[1].Name); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetScopes_ReportsNamedVariableCount() + { + using (CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + exprValues["env"] = new DictionaryContextData + { + { "A", new StringContextData("1") }, + { "B", new StringContextData("2") }, + { "C", new StringContextData("3") } + }; + + var ctx = CreateMockContext(exprValues); + var scopes = _provider.GetScopes(ctx.Object); + + Assert.Single(scopes); + Assert.Equal(3, scopes[0].NamedVariables); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetScopes_SecretsGetSpecialPresentationHint() + { + using (CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + exprValues["secrets"] = new DictionaryContextData + { + { "MY_SECRET", new StringContextData("super-secret") } + }; + exprValues["env"] = new DictionaryContextData + { + { "CI", new StringContextData("true") } + }; + + var ctx = CreateMockContext(exprValues); + var scopes = _provider.GetScopes(ctx.Object); + + var envScope = scopes.Find(s => s.Name == "env"); + var secretsScope = scopes.Find(s => s.Name == "secrets"); + + Assert.NotNull(envScope); + Assert.Null(envScope.PresentationHint); + + Assert.NotNull(secretsScope); + Assert.Equal("registers", secretsScope.PresentationHint); + } + } + + #endregion + + #region GetVariables — basic types + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetVariables_ReturnsEmptyWhenContextIsNull() + { + using (CreateTestContext()) + { + var variables = _provider.GetVariables(null, 1); + Assert.Empty(variables); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetVariables_ReturnsStringVariables() + { + using (CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + exprValues["env"] = new DictionaryContextData + { + { "CI", new StringContextData("true") }, + { "HOME", new StringContextData("/home/runner") } + }; + + var ctx = CreateMockContext(exprValues); + // "env" is at ScopeNames index 1 → variablesReference = 2 + var variables = _provider.GetVariables(ctx.Object, 2); + + Assert.Equal(2, variables.Count); + + var ciVar = variables.Find(v => v.Name == "CI"); + Assert.NotNull(ciVar); + Assert.Equal("true", ciVar.Value); + Assert.Equal("string", ciVar.Type); + Assert.Equal(0, ciVar.VariablesReference); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetVariables_ReturnsBooleanVariables() + { + using (CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + exprValues["github"] = new DictionaryContextData + { + { "event_name", new StringContextData("push") }, + }; + // Use a nested dict with boolean to test + var jobDict = new DictionaryContextData(); + // BooleanContextData is a valid PipelineContextData type + // but job context typically has strings. Use env scope instead. + exprValues["env"] = new DictionaryContextData + { + { "flag", new BooleanContextData(true) } + }; + + var ctx = CreateMockContext(exprValues); + // "env" is at index 1 → ref 2 + var variables = _provider.GetVariables(ctx.Object, 2); + + var flagVar = variables.Find(v => v.Name == "flag"); + Assert.NotNull(flagVar); + Assert.Equal("true", flagVar.Value); + Assert.Equal("boolean", flagVar.Type); + Assert.Equal(0, flagVar.VariablesReference); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetVariables_ReturnsNumberVariables() + { + using (CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + exprValues["env"] = new DictionaryContextData + { + { "count", new NumberContextData(42) } + }; + + var ctx = CreateMockContext(exprValues); + var variables = _provider.GetVariables(ctx.Object, 2); + + var countVar = variables.Find(v => v.Name == "count"); + Assert.NotNull(countVar); + Assert.Equal("42", countVar.Value); + Assert.Equal("number", countVar.Type); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetVariables_HandlesNullValues() + { + using (CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + var dict = new DictionaryContextData(); + dict["present"] = new StringContextData("yes"); + dict["missing"] = null; + exprValues["env"] = dict; + + var ctx = CreateMockContext(exprValues); + var variables = _provider.GetVariables(ctx.Object, 2); + + var nullVar = variables.Find(v => v.Name == "missing"); + Assert.NotNull(nullVar); + Assert.Equal("null", nullVar.Value); + Assert.Equal("null", nullVar.Type); + Assert.Equal(0, nullVar.VariablesReference); + } + } + + #endregion + + #region GetVariables — nested expansion + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetVariables_NestedDictionaryIsExpandable() + { + using (CreateTestContext()) + { + var innerDict = new DictionaryContextData + { + { "name", new StringContextData("push") }, + { "ref", new StringContextData("refs/heads/main") } + }; + var exprValues = new DictionaryContextData(); + exprValues["github"] = new DictionaryContextData + { + { "event", innerDict } + }; + + var ctx = CreateMockContext(exprValues); + // "github" is at index 0 → ref 1 + var variables = _provider.GetVariables(ctx.Object, 1); + + var eventVar = variables.Find(v => v.Name == "event"); + Assert.NotNull(eventVar); + Assert.Equal("object", eventVar.Type); + Assert.True(eventVar.VariablesReference > 0, "Nested dict should have a non-zero variablesReference"); + Assert.Equal(2, eventVar.NamedVariables); + + // Now expand it + var children = _provider.GetVariables(ctx.Object, eventVar.VariablesReference); + Assert.Equal(2, children.Count); + + var nameVar = children.Find(v => v.Name == "name"); + Assert.NotNull(nameVar); + Assert.Equal("push", nameVar.Value); + Assert.Equal("${{ github.event.name }}", nameVar.EvaluateName); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetVariables_NestedArrayIsExpandable() + { + using (CreateTestContext()) + { + var array = new ArrayContextData(); + array.Add(new StringContextData("item0")); + array.Add(new StringContextData("item1")); + + var exprValues = new DictionaryContextData(); + exprValues["env"] = new DictionaryContextData + { + { "list", array } + }; + + var ctx = CreateMockContext(exprValues); + var variables = _provider.GetVariables(ctx.Object, 2); + + var listVar = variables.Find(v => v.Name == "list"); + Assert.NotNull(listVar); + Assert.Equal("array", listVar.Type); + Assert.True(listVar.VariablesReference > 0); + Assert.Equal(2, listVar.IndexedVariables); + + // Expand the array + var items = _provider.GetVariables(ctx.Object, listVar.VariablesReference); + Assert.Equal(2, items.Count); + Assert.Equal("[0]", items[0].Name); + Assert.Equal("item0", items[0].Value); + Assert.Equal("[1]", items[1].Name); + Assert.Equal("item1", items[1].Value); + } + } + + #endregion + + #region Secret masking + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetVariables_SecretsScopeValuesAreRedacted() + { + using (CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + exprValues["secrets"] = new DictionaryContextData + { + { "MY_TOKEN", new StringContextData("ghp_abc123secret") }, + { "DB_PASSWORD", new StringContextData("p@ssword!") } + }; + + var ctx = CreateMockContext(exprValues); + // "secrets" is at index 5 → ref 6 + var variables = _provider.GetVariables(ctx.Object, 6); + + Assert.Equal(2, variables.Count); + foreach (var v in variables) + { + Assert.Equal("***", v.Value); + Assert.Equal("string", v.Type); + } + + // Keys should still be visible + Assert.Contains(variables, v => v.Name == "MY_TOKEN"); + Assert.Contains(variables, v => v.Name == "DB_PASSWORD"); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetVariables_NonSecretScopeValuesMaskedBySecretMasker() + { + using (var hc = CreateTestContext()) + { + // Register a known secret value with the masker + hc.SecretMasker.AddValue("super-secret-token"); + + var exprValues = new DictionaryContextData(); + exprValues["env"] = new DictionaryContextData + { + { "SAFE", new StringContextData("hello world") }, + { "LEAKED", new StringContextData("prefix-super-secret-token-suffix") } + }; + + var ctx = CreateMockContext(exprValues); + var variables = _provider.GetVariables(ctx.Object, 2); + + var safeVar = variables.Find(v => v.Name == "SAFE"); + Assert.NotNull(safeVar); + Assert.Equal("hello world", safeVar.Value); + + var leakedVar = variables.Find(v => v.Name == "LEAKED"); + Assert.NotNull(leakedVar); + Assert.DoesNotContain("super-secret-token", leakedVar.Value); + Assert.Contains("***", leakedVar.Value); + } + } + + #endregion + + #region Reset + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Reset_InvalidatesNestedReferences() + { + using (CreateTestContext()) + { + var innerDict = new DictionaryContextData + { + { "name", new StringContextData("push") } + }; + var exprValues = new DictionaryContextData(); + exprValues["github"] = new DictionaryContextData + { + { "event", innerDict } + }; + + var ctx = CreateMockContext(exprValues); + var variables = _provider.GetVariables(ctx.Object, 1); + var eventVar = variables.Find(v => v.Name == "event"); + Assert.True(eventVar.VariablesReference > 0); + + var savedRef = eventVar.VariablesReference; + + // Reset should clear all dynamic references + _provider.Reset(); + + var children = _provider.GetVariables(ctx.Object, savedRef); + Assert.Empty(children); + } + } + + #endregion + + #region EvaluateName + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetVariables_SetsEvaluateNameWithDotPath() + { + using (CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + exprValues["github"] = new DictionaryContextData + { + { "repository", new StringContextData("owner/repo") } + }; + + var ctx = CreateMockContext(exprValues); + var variables = _provider.GetVariables(ctx.Object, 1); + + var repoVar = variables.Find(v => v.Name == "repository"); + Assert.NotNull(repoVar); + Assert.Equal("${{ github.repository }}", repoVar.EvaluateName); + } + } + + #endregion + + #region EvaluateExpression + + /// + /// Creates a mock execution context with Global set up so that + /// ToPipelineTemplateEvaluator() works for real expression evaluation. + /// + private Moq.Mock CreateEvaluatableContext( + TestHostContext hc, + DictionaryContextData expressionValues) + { + var mock = new Moq.Mock(); + mock.Setup(x => x.ExpressionValues).Returns(expressionValues); + mock.Setup(x => x.ExpressionFunctions) + .Returns(new List()); + mock.Setup(x => x.Global).Returns(new GlobalContext + { + FileTable = new List(), + Variables = new Variables(hc, new Dictionary()), + }); + // ToPipelineTemplateEvaluator uses ToTemplateTraceWriter which calls + // context.Write — provide a no-op so it doesn't NRE. + mock.Setup(x => x.Write(Moq.It.IsAny(), Moq.It.IsAny())); + return mock; + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void EvaluateExpression_ReturnsValueForSimpleExpression() + { + using (var hc = CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + exprValues["github"] = new DictionaryContextData + { + { "repository", new StringContextData("owner/repo") } + }; + + var ctx = CreateEvaluatableContext(hc, exprValues); + var result = _provider.EvaluateExpression("github.repository", ctx.Object); + + Assert.Equal("owner/repo", result.Result); + Assert.Equal("string", result.Type); + Assert.Equal(0, result.VariablesReference); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void EvaluateExpression_StripsWrapperSyntax() + { + using (var hc = CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + exprValues["github"] = new DictionaryContextData + { + { "event_name", new StringContextData("push") } + }; + + var ctx = CreateEvaluatableContext(hc, exprValues); + var result = _provider.EvaluateExpression("${{ github.event_name }}", ctx.Object); + + Assert.Equal("push", result.Result); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void EvaluateExpression_MasksSecretInResult() + { + using (var hc = CreateTestContext()) + { + hc.SecretMasker.AddValue("super-secret"); + + var exprValues = new DictionaryContextData(); + exprValues["env"] = new DictionaryContextData + { + { "TOKEN", new StringContextData("super-secret") } + }; + + var ctx = CreateEvaluatableContext(hc, exprValues); + var result = _provider.EvaluateExpression("env.TOKEN", ctx.Object); + + Assert.DoesNotContain("super-secret", result.Result); + Assert.Contains("***", result.Result); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void EvaluateExpression_ReturnsErrorForInvalidExpression() + { + using (var hc = CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + exprValues["github"] = new DictionaryContextData(); + + var ctx = CreateEvaluatableContext(hc, exprValues); + // An invalid expression syntax should not throw — it should + // return an error result. + var result = _provider.EvaluateExpression("!!!invalid[[", ctx.Object); + + Assert.Contains("error", result.Result, StringComparison.OrdinalIgnoreCase); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void EvaluateExpression_ReturnsMessageWhenNoContext() + { + using (CreateTestContext()) + { + var result = _provider.EvaluateExpression("github.repository", null); + + Assert.Contains("no execution context", result.Result, StringComparison.OrdinalIgnoreCase); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void EvaluateExpression_ReturnsEmptyForEmptyExpression() + { + using (var hc = CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + var ctx = CreateEvaluatableContext(hc, exprValues); + var result = _provider.EvaluateExpression("", ctx.Object); + + Assert.Equal(string.Empty, result.Result); + } + } + + #endregion + + #region InferResultType + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void InferResultType_ClassifiesCorrectly() + { + using (CreateTestContext()) + { + Assert.Equal("null", DapVariableProvider.InferResultType(null)); + Assert.Equal("null", DapVariableProvider.InferResultType("null")); + Assert.Equal("boolean", DapVariableProvider.InferResultType("true")); + Assert.Equal("boolean", DapVariableProvider.InferResultType("false")); + Assert.Equal("number", DapVariableProvider.InferResultType("42")); + Assert.Equal("number", DapVariableProvider.InferResultType("3.14")); + Assert.Equal("object", DapVariableProvider.InferResultType("{\"key\":\"val\"}")); + Assert.Equal("object", DapVariableProvider.InferResultType("[1,2,3]")); + Assert.Equal("string", DapVariableProvider.InferResultType("hello world")); + Assert.Equal("string", DapVariableProvider.InferResultType("owner/repo")); + } + } + + #endregion + + #region Non-string secret type redaction + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetVariables_SecretsScopeRedactsNumberContextData() + { + using (CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + exprValues["secrets"] = new DictionaryContextData + { + { "NUMERIC_SECRET", new NumberContextData(12345) } + }; + + var ctx = CreateMockContext(exprValues); + var variables = _provider.GetVariables(ctx.Object, 6); + + Assert.Single(variables); + Assert.Equal("NUMERIC_SECRET", variables[0].Name); + Assert.Equal("***", variables[0].Value); + Assert.Equal("string", variables[0].Type); + Assert.Equal(0, variables[0].VariablesReference); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetVariables_SecretsScopeRedactsBooleanContextData() + { + using (CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + exprValues["secrets"] = new DictionaryContextData + { + { "BOOL_SECRET", new BooleanContextData(true) } + }; + + var ctx = CreateMockContext(exprValues); + var variables = _provider.GetVariables(ctx.Object, 6); + + Assert.Single(variables); + Assert.Equal("BOOL_SECRET", variables[0].Name); + Assert.Equal("***", variables[0].Value); + Assert.Equal("string", variables[0].Type); + Assert.Equal(0, variables[0].VariablesReference); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetVariables_SecretsScopeRedactsNestedDictionary() + { + using (CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + exprValues["secrets"] = new DictionaryContextData + { + { "NESTED_SECRET", new DictionaryContextData + { + { "inner_key", new StringContextData("inner_value") } + } + } + }; + + var ctx = CreateMockContext(exprValues); + var variables = _provider.GetVariables(ctx.Object, 6); + + Assert.Single(variables); + Assert.Equal("NESTED_SECRET", variables[0].Name); + Assert.Equal("***", variables[0].Value); + Assert.Equal("string", variables[0].Type); + // Nested container should NOT be drillable under secrets + Assert.Equal(0, variables[0].VariablesReference); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void GetVariables_SecretsScopeRedactsNullValue() + { + using (CreateTestContext()) + { + var exprValues = new DictionaryContextData(); + var secrets = new DictionaryContextData(); + secrets["NULL_SECRET"] = null; + exprValues["secrets"] = secrets; + + var ctx = CreateMockContext(exprValues); + var variables = _provider.GetVariables(ctx.Object, 6); + + Assert.Single(variables); + Assert.Equal("NULL_SECRET", variables[0].Name); + Assert.Equal("***", variables[0].Value); + Assert.Equal(0, variables[0].VariablesReference); + } + } + + #endregion + } +} diff --git a/src/Test/L0/Worker/JobExtensionL0.cs b/src/Test/L0/Worker/JobExtensionL0.cs index 60814998e..40c495a81 100644 --- a/src/Test/L0/Worker/JobExtensionL0.cs +++ b/src/Test/L0/Worker/JobExtensionL0.cs @@ -8,6 +8,7 @@ using GitHub.DistributedTask.ObjectTemplating.Tokens; using GitHub.DistributedTask.Pipelines.ObjectTemplating; using GitHub.DistributedTask.WebApi; using GitHub.Runner.Worker; +using GitHub.Runner.Worker.Dap; using Moq; using Xunit; using Pipelines = GitHub.DistributedTask.Pipelines; @@ -547,6 +548,10 @@ namespace GitHub.Runner.Common.Tests.Worker var _stepsRunner = new StepsRunner(); _stepsRunner.Initialize(hc); + + var mockDapDebugger = new Mock(); + hc.SetSingleton(mockDapDebugger.Object); + await _stepsRunner.RunAsync(_jobEc); Assert.Equal("Create custom image", snapshotStep.DisplayName); diff --git a/src/Test/L0/Worker/StepsRunnerL0.cs b/src/Test/L0/Worker/StepsRunnerL0.cs index a22dc618f..2ab9f57fd 100644 --- a/src/Test/L0/Worker/StepsRunnerL0.cs +++ b/src/Test/L0/Worker/StepsRunnerL0.cs @@ -12,6 +12,7 @@ using GitHub.DistributedTask.ObjectTemplating.Tokens; using GitHub.DistributedTask.WebApi; using GitHub.Runner.Common.Util; using GitHub.Runner.Worker; +using GitHub.Runner.Worker.Dap; namespace GitHub.Runner.Common.Tests.Worker { @@ -61,6 +62,10 @@ namespace GitHub.Runner.Common.Tests.Worker _stepsRunner = new StepsRunner(); _stepsRunner.Initialize(hc); + + var mockDapDebugger = new Mock(); + hc.SetSingleton(mockDapDebugger.Object); + return hc; } From 9728019b24400dd2d99b1ad5e5622a218d588360 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:40:31 +0000 Subject: [PATCH 03/15] Bump @typescript-eslint/eslint-plugin from 8.57.1 to 8.57.2 in /src/Misc/expressionFunc/hashFiles (#4310) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../hashFiles/package-lock.json | 252 +++++++++--------- .../expressionFunc/hashFiles/package.json | 2 +- 2 files changed, 127 insertions(+), 127 deletions(-) diff --git a/src/Misc/expressionFunc/hashFiles/package-lock.json b/src/Misc/expressionFunc/hashFiles/package-lock.json index bfabff2f0..d8099aa52 100644 --- a/src/Misc/expressionFunc/hashFiles/package-lock.json +++ b/src/Misc/expressionFunc/hashFiles/package-lock.json @@ -14,7 +14,7 @@ "devDependencies": { "@stylistic/eslint-plugin": "^5.10.0", "@types/node": "^22.0.0", - "@typescript-eslint/eslint-plugin": "^8.57.1", + "@typescript-eslint/eslint-plugin": "^8.57.2", "@typescript-eslint/parser": "^8.0.0", "@vercel/ncc": "^0.38.3", "eslint": "^8.47.0", @@ -308,16 +308,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", - "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/type-utils": "8.57.1", - "@typescript-eslint/utils": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -330,7 +330,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.57.1", + "@typescript-eslint/parser": "^8.57.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } @@ -358,15 +358,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", - "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" }, "engines": { @@ -382,13 +382,13 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", - "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.1", - "@typescript-eslint/types": "^8.57.1", + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" }, "engines": { @@ -403,13 +403,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", - "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -420,9 +420,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", - "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -436,14 +436,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", - "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -460,9 +460,9 @@ } }, "node_modules/@typescript-eslint/type-utils/node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "engines": { "node": ">=18.12" @@ -472,9 +472,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", - "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -485,15 +485,15 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", - "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", "dev": true, "dependencies": { - "@typescript-eslint/project-service": "8.57.1", - "@typescript-eslint/tsconfig-utils": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -548,9 +548,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "engines": { "node": ">=18.12" @@ -560,15 +560,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", - "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1" + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -583,12 +583,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", - "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -4323,9 +4323,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "engines": { "node": ">=12" @@ -4923,16 +4923,16 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", - "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/type-utils": "8.57.1", - "@typescript-eslint/utils": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -4954,84 +4954,84 @@ } }, "@typescript-eslint/parser": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", - "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3" } }, "@typescript-eslint/project-service": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", - "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "requires": { - "@typescript-eslint/tsconfig-utils": "^8.57.1", - "@typescript-eslint/types": "^8.57.1", + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", "debug": "^4.4.3" } }, "@typescript-eslint/scope-manager": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", - "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", "dev": true, "requires": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" } }, "@typescript-eslint/tsconfig-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", - "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", "dev": true, "requires": {} }, "@typescript-eslint/type-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", - "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", "dev": true, "requires": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "dependencies": { "ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "requires": {} } } }, "@typescript-eslint/types": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", - "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", - "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", "dev": true, "requires": { - "@typescript-eslint/project-service": "8.57.1", - "@typescript-eslint/tsconfig-utils": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -5064,33 +5064,33 @@ } }, "ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "requires": {} } } }, "@typescript-eslint/utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", - "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1" + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" } }, "@typescript-eslint/visitor-keys": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", - "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", "dev": true, "requires": { - "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/types": "8.57.2", "eslint-visitor-keys": "^5.0.0" }, "dependencies": { @@ -7612,9 +7612,9 @@ "requires": {} }, "picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true } } diff --git a/src/Misc/expressionFunc/hashFiles/package.json b/src/Misc/expressionFunc/hashFiles/package.json index 87b461448..0097a84b7 100644 --- a/src/Misc/expressionFunc/hashFiles/package.json +++ b/src/Misc/expressionFunc/hashFiles/package.json @@ -37,7 +37,7 @@ "devDependencies": { "@stylistic/eslint-plugin": "^5.10.0", "@types/node": "^22.0.0", - "@typescript-eslint/eslint-plugin": "^8.57.1", + "@typescript-eslint/eslint-plugin": "^8.57.2", "@typescript-eslint/parser": "^8.0.0", "@vercel/ncc": "^0.38.3", "eslint": "^8.47.0", From f0c228635e874e39eb53db0bf091c4925e53cbb1 Mon Sep 17 00:00:00 2001 From: eric sciple Date: Fri, 27 Mar 2026 11:45:42 -0500 Subject: [PATCH 04/15] Remove AllowCaseFunction feature flag (#4316) --- src/Runner.Worker/ActionManifestManager.cs | 1 - .../ActionManifestManagerLegacy.cs | 3 +- .../Expressions2/ExpressionParser.cs | 16 ++------ .../ObjectTemplating/TemplateContext.cs | 6 --- .../ObjectTemplating/Tokens/TemplateToken.cs | 8 ++-- .../PipelineTemplateConverter.cs | 2 +- src/Sdk/Expressions/ExpressionParser.cs | 20 +++------ .../Conversion/WorkflowTemplateConverter.cs | 2 +- .../ObjectTemplating/TemplateContext.cs | 8 +--- .../ObjectTemplating/Tokens/TemplateToken.cs | 12 +++--- src/Test/L0/Sdk/ExpressionParserL0.cs | 32 ++++----------- src/Test/L0/Worker/ActionManifestManagerL0.cs | 41 ++++++++++++++++++- 12 files changed, 69 insertions(+), 82 deletions(-) diff --git a/src/Runner.Worker/ActionManifestManager.cs b/src/Runner.Worker/ActionManifestManager.cs index a70592381..014c053aa 100644 --- a/src/Runner.Worker/ActionManifestManager.cs +++ b/src/Runner.Worker/ActionManifestManager.cs @@ -316,7 +316,6 @@ namespace GitHub.Runner.Worker Schema = _actionManifestSchema, // TODO: Switch to real tracewriter for cutover TraceWriter = new GitHub.Actions.WorkflowParser.ObjectTemplating.EmptyTraceWriter(), - AllowCaseFunction = false, }; // Expression values from execution context diff --git a/src/Runner.Worker/ActionManifestManagerLegacy.cs b/src/Runner.Worker/ActionManifestManagerLegacy.cs index c332efd2e..d423aff86 100644 --- a/src/Runner.Worker/ActionManifestManagerLegacy.cs +++ b/src/Runner.Worker/ActionManifestManagerLegacy.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Threading; @@ -315,7 +315,6 @@ namespace GitHub.Runner.Worker maxBytes: 10 * 1024 * 1024), Schema = _actionManifestSchema, TraceWriter = executionContext.ToTemplateTraceWriter(), - AllowCaseFunction = false, }; // Expression values from execution context diff --git a/src/Sdk/DTExpressions2/Expressions2/ExpressionParser.cs b/src/Sdk/DTExpressions2/Expressions2/ExpressionParser.cs index f0dac3d24..95c6698c0 100644 --- a/src/Sdk/DTExpressions2/Expressions2/ExpressionParser.cs +++ b/src/Sdk/DTExpressions2/Expressions2/ExpressionParser.cs @@ -17,10 +17,9 @@ namespace GitHub.DistributedTask.Expressions2 String expression, ITraceWriter trace, IEnumerable namedValues, - IEnumerable functions, - Boolean allowCaseFunction = true) + IEnumerable functions) { - var context = new ParseContext(expression, trace, namedValues, functions, allowCaseFunction: allowCaseFunction); + var context = new ParseContext(expression, trace, namedValues, functions); context.Trace.Info($"Parsing expression: <{expression}>"); return CreateTree(context); } @@ -416,12 +415,6 @@ namespace GitHub.DistributedTask.Expressions2 String name, out IFunctionInfo functionInfo) { - if (String.Equals(name, "case", StringComparison.OrdinalIgnoreCase) && !context.AllowCaseFunction) - { - functionInfo = null; - return false; - } - return ExpressionConstants.WellKnownFunctions.TryGetValue(name, out functionInfo) || context.ExtensionFunctions.TryGetValue(name, out functionInfo); } @@ -429,7 +422,6 @@ namespace GitHub.DistributedTask.Expressions2 private sealed class ParseContext { public Boolean AllowUnknownKeywords; - public Boolean AllowCaseFunction; public readonly String Expression; public readonly Dictionary ExtensionFunctions = new Dictionary(StringComparer.OrdinalIgnoreCase); public readonly Dictionary ExtensionNamedValues = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -445,8 +437,7 @@ namespace GitHub.DistributedTask.Expressions2 ITraceWriter trace, IEnumerable namedValues, IEnumerable functions, - Boolean allowUnknownKeywords = false, - Boolean allowCaseFunction = true) + Boolean allowUnknownKeywords = false) { Expression = expression ?? String.Empty; if (Expression.Length > ExpressionConstants.MaxLength) @@ -467,7 +458,6 @@ namespace GitHub.DistributedTask.Expressions2 LexicalAnalyzer = new LexicalAnalyzer(Expression); AllowUnknownKeywords = allowUnknownKeywords; - AllowCaseFunction = allowCaseFunction; } private class NoOperationTraceWriter : ITraceWriter diff --git a/src/Sdk/DTObjectTemplating/ObjectTemplating/TemplateContext.cs b/src/Sdk/DTObjectTemplating/ObjectTemplating/TemplateContext.cs index d93eda398..af8cf76ec 100644 --- a/src/Sdk/DTObjectTemplating/ObjectTemplating/TemplateContext.cs +++ b/src/Sdk/DTObjectTemplating/ObjectTemplating/TemplateContext.cs @@ -86,12 +86,6 @@ namespace GitHub.DistributedTask.ObjectTemplating internal ITraceWriter TraceWriter { get; set; } - /// - /// Gets or sets a value indicating whether the case expression function is allowed. - /// Defaults to true. Set to false to disable the case function. - /// - internal Boolean AllowCaseFunction { get; set; } = true; - private IDictionary FileIds { get diff --git a/src/Sdk/DTObjectTemplating/ObjectTemplating/Tokens/TemplateToken.cs b/src/Sdk/DTObjectTemplating/ObjectTemplating/Tokens/TemplateToken.cs index 870163b76..21a8aab78 100644 --- a/src/Sdk/DTObjectTemplating/ObjectTemplating/Tokens/TemplateToken.cs +++ b/src/Sdk/DTObjectTemplating/ObjectTemplating/Tokens/TemplateToken.cs @@ -57,7 +57,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens var originalBytes = context.Memory.CurrentBytes; try { - var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction); + var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions); var options = new EvaluationOptions { MaxMemory = context.Memory.MaxBytes, @@ -94,7 +94,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens var originalBytes = context.Memory.CurrentBytes; try { - var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction); + var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions); var options = new EvaluationOptions { MaxMemory = context.Memory.MaxBytes, @@ -123,7 +123,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens var originalBytes = context.Memory.CurrentBytes; try { - var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction); + var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions); var options = new EvaluationOptions { MaxMemory = context.Memory.MaxBytes, @@ -152,7 +152,7 @@ namespace GitHub.DistributedTask.ObjectTemplating.Tokens var originalBytes = context.Memory.CurrentBytes; try { - var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction); + var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions); var options = new EvaluationOptions { MaxMemory = context.Memory.MaxBytes, diff --git a/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConverter.cs b/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConverter.cs index 87bb00bae..38176eb36 100644 --- a/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConverter.cs +++ b/src/Sdk/DTPipelines/Pipelines/ObjectTemplating/PipelineTemplateConverter.cs @@ -681,7 +681,7 @@ namespace GitHub.DistributedTask.Pipelines.ObjectTemplating var node = default(ExpressionNode); try { - node = expressionParser.CreateTree(condition, null, namedValues, functions, allowCaseFunction: context.AllowCaseFunction) as ExpressionNode; + node = expressionParser.CreateTree(condition, null, namedValues, functions) as ExpressionNode; } catch (Exception ex) { diff --git a/src/Sdk/Expressions/ExpressionParser.cs b/src/Sdk/Expressions/ExpressionParser.cs index fd7dd1b45..3c4ad104f 100644 --- a/src/Sdk/Expressions/ExpressionParser.cs +++ b/src/Sdk/Expressions/ExpressionParser.cs @@ -1,4 +1,4 @@ -#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references +#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references using System; using System.Collections.Generic; @@ -17,10 +17,9 @@ namespace GitHub.Actions.Expressions String expression, ITraceWriter trace, IEnumerable namedValues, - IEnumerable functions, - Boolean allowCaseFunction = true) + IEnumerable functions) { - var context = new ParseContext(expression, trace, namedValues, functions, allowCaseFunction: allowCaseFunction); + var context = new ParseContext(expression, trace, namedValues, functions); context.Trace.Info($"Parsing expression: <{expression}>"); return CreateTree(context); } @@ -322,7 +321,7 @@ namespace GitHub.Actions.Expressions context.Operators.Pop(); } var functionOperands = PopOperands(context, parameterCount); - + // Node already exists on the operand stack function = (Function)context.Operands.Peek(); @@ -416,12 +415,6 @@ namespace GitHub.Actions.Expressions String name, out IFunctionInfo functionInfo) { - if (String.Equals(name, "case", StringComparison.OrdinalIgnoreCase) && !context.AllowCaseFunction) - { - functionInfo = null; - return false; - } - return ExpressionConstants.WellKnownFunctions.TryGetValue(name, out functionInfo) || context.ExtensionFunctions.TryGetValue(name, out functionInfo); } @@ -429,7 +422,6 @@ namespace GitHub.Actions.Expressions private sealed class ParseContext { public Boolean AllowUnknownKeywords; - public Boolean AllowCaseFunction; public readonly String Expression; public readonly Dictionary ExtensionFunctions = new Dictionary(StringComparer.OrdinalIgnoreCase); public readonly Dictionary ExtensionNamedValues = new Dictionary(StringComparer.OrdinalIgnoreCase); @@ -445,8 +437,7 @@ namespace GitHub.Actions.Expressions ITraceWriter trace, IEnumerable namedValues, IEnumerable functions, - Boolean allowUnknownKeywords = false, - Boolean allowCaseFunction = true) + Boolean allowUnknownKeywords = false) { Expression = expression ?? String.Empty; if (Expression.Length > ExpressionConstants.MaxLength) @@ -467,7 +458,6 @@ namespace GitHub.Actions.Expressions LexicalAnalyzer = new LexicalAnalyzer(Expression); AllowUnknownKeywords = allowUnknownKeywords; - AllowCaseFunction = allowCaseFunction; } private class NoOperationTraceWriter : ITraceWriter diff --git a/src/Sdk/WorkflowParser/Conversion/WorkflowTemplateConverter.cs b/src/Sdk/WorkflowParser/Conversion/WorkflowTemplateConverter.cs index 8ae6ea0c9..0c53f87fc 100644 --- a/src/Sdk/WorkflowParser/Conversion/WorkflowTemplateConverter.cs +++ b/src/Sdk/WorkflowParser/Conversion/WorkflowTemplateConverter.cs @@ -1828,7 +1828,7 @@ namespace GitHub.Actions.WorkflowParser.Conversion var node = default(ExpressionNode); try { - node = expressionParser.CreateTree(condition, null, namedValues, functions, allowCaseFunction: context.AllowCaseFunction) as ExpressionNode; + node = expressionParser.CreateTree(condition, null, namedValues, functions) as ExpressionNode; } catch (Exception ex) { diff --git a/src/Sdk/WorkflowParser/ObjectTemplating/TemplateContext.cs b/src/Sdk/WorkflowParser/ObjectTemplating/TemplateContext.cs index 6e35e850b..13a11ca2c 100644 --- a/src/Sdk/WorkflowParser/ObjectTemplating/TemplateContext.cs +++ b/src/Sdk/WorkflowParser/ObjectTemplating/TemplateContext.cs @@ -1,4 +1,4 @@ -#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references +#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references using System; using System.Collections.Generic; @@ -113,12 +113,6 @@ namespace GitHub.Actions.WorkflowParser.ObjectTemplating /// internal Boolean StrictJsonParsing { get; set; } - /// - /// Gets or sets a value indicating whether the case expression function is allowed. - /// Defaults to true. Set to false to disable the case function. - /// - internal Boolean AllowCaseFunction { get; set; } = true; - internal ITraceWriter TraceWriter { get; set; } private IDictionary FileIds diff --git a/src/Sdk/WorkflowParser/ObjectTemplating/Tokens/TemplateToken.cs b/src/Sdk/WorkflowParser/ObjectTemplating/Tokens/TemplateToken.cs index 0c1295ccb..fe90b4957 100644 --- a/src/Sdk/WorkflowParser/ObjectTemplating/Tokens/TemplateToken.cs +++ b/src/Sdk/WorkflowParser/ObjectTemplating/Tokens/TemplateToken.cs @@ -1,4 +1,4 @@ -#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references +#nullable disable // Consider removing in the future to minimize likelihood of NullReferenceException; refer https://learn.microsoft.com/en-us/dotnet/csharp/nullable-references using System; using System.Collections.Generic; @@ -55,7 +55,7 @@ namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens var originalBytes = context.Memory.CurrentBytes; try { - var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction); + var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions); var options = new EvaluationOptions { MaxMemory = context.Memory.MaxBytes, @@ -93,7 +93,7 @@ namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens var originalBytes = context.Memory.CurrentBytes; try { - var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction); + var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions); var options = new EvaluationOptions { MaxMemory = context.Memory.MaxBytes, @@ -123,7 +123,7 @@ namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens var originalBytes = context.Memory.CurrentBytes; try { - var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction); + var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions); var options = new EvaluationOptions { MaxMemory = context.Memory.MaxBytes, @@ -153,7 +153,7 @@ namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens var originalBytes = context.Memory.CurrentBytes; try { - var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions, allowCaseFunction: context.AllowCaseFunction); + var tree = new ExpressionParser().CreateTree(expression, null, context.GetExpressionNamedValues(), context.ExpressionFunctions); var options = new EvaluationOptions { MaxMemory = context.Memory.MaxBytes, @@ -289,4 +289,4 @@ namespace GitHub.Actions.WorkflowParser.ObjectTemplating.Tokens return result; } } -} \ No newline at end of file +} diff --git a/src/Test/L0/Sdk/ExpressionParserL0.cs b/src/Test/L0/Sdk/ExpressionParserL0.cs index 6512ec9e7..313d038cd 100644 --- a/src/Test/L0/Sdk/ExpressionParserL0.cs +++ b/src/Test/L0/Sdk/ExpressionParserL0.cs @@ -1,4 +1,4 @@ -using GitHub.DistributedTask.Expressions2; +using GitHub.DistributedTask.Expressions2; using GitHub.DistributedTask.Expressions2.Sdk; using GitHub.DistributedTask.ObjectTemplating; using System; @@ -9,7 +9,7 @@ namespace GitHub.Runner.Common.Tests.Sdk { /// /// Regression tests for ExpressionParser.CreateTree to verify that - /// allowCaseFunction does not accidentally set allowUnknownKeywords. + /// the case function does not accidentally set allowUnknownKeywords. /// public sealed class ExpressionParserL0 { @@ -18,7 +18,7 @@ namespace GitHub.Runner.Common.Tests.Sdk [Trait("Category", "Sdk")] public void CreateTree_RejectsUnrecognizedNamedValue() { - // Regression: allowCaseFunction was passed positionally into + // Regression: the case function parameter was passed positionally into // the allowUnknownKeywords parameter, causing all named values // to be silently accepted. var parser = new ExpressionParser(); @@ -52,7 +52,7 @@ namespace GitHub.Runner.Common.Tests.Sdk [Fact] [Trait("Level", "L0")] [Trait("Category", "Sdk")] - public void CreateTree_CaseFunctionWorks_WhenAllowed() + public void CreateTree_CaseFunctionWorks() { var parser = new ExpressionParser(); var namedValues = new List @@ -60,35 +60,17 @@ namespace GitHub.Runner.Common.Tests.Sdk new NamedValueInfo("github"), }; - var node = parser.CreateTree("case(github.event_name, 'push', 'Push Event')", null, namedValues, null, allowCaseFunction: true); + var node = parser.CreateTree("case(github.event_name, 'push', 'Push Event')", null, namedValues, null); Assert.NotNull(node); } - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Sdk")] - public void CreateTree_CaseFunctionRejected_WhenDisallowed() - { - var parser = new ExpressionParser(); - var namedValues = new List - { - new NamedValueInfo("github"), - }; - - var ex = Assert.Throws(() => - parser.CreateTree("case(github.event_name, 'push', 'Push Event')", null, namedValues, null, allowCaseFunction: false)); - - Assert.Contains("Unrecognized function", ex.Message); - } - [Fact] [Trait("Level", "L0")] [Trait("Category", "Sdk")] public void CreateTree_CaseFunctionDoesNotAffectUnknownKeywords() { - // The key regression test: with allowCaseFunction=true (default), - // unrecognized named values must still be rejected. + // The key regression test: unrecognized named values must still be rejected. var parser = new ExpressionParser(); var namedValues = new List { @@ -96,7 +78,7 @@ namespace GitHub.Runner.Common.Tests.Sdk }; var ex = Assert.Throws(() => - parser.CreateTree("github.ref", null, namedValues, null, allowCaseFunction: true)); + parser.CreateTree("github.ref", null, namedValues, null)); Assert.Contains("Unrecognized named-value", ex.Message); } diff --git a/src/Test/L0/Worker/ActionManifestManagerL0.cs b/src/Test/L0/Worker/ActionManifestManagerL0.cs index 9d707c3da..6a3da0c72 100644 --- a/src/Test/L0/Worker/ActionManifestManagerL0.cs +++ b/src/Test/L0/Worker/ActionManifestManagerL0.cs @@ -504,7 +504,7 @@ namespace GitHub.Runner.Common.Tests.Worker } } - [Fact] + [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] public void Load_Node24Action() @@ -1006,6 +1006,45 @@ namespace GitHub.Runner.Common.Tests.Worker _ec.Setup(x => x.AddIssue(It.IsAny(), It.IsAny())).Callback((Issue issue, ExecutionContextLogOptions logOptions) => { _hc.GetTrace().Info($"[{issue.Type}]{logOptions.LogMessageOverride ?? issue.Message}"); }); } + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void Evaluate_Default_Input_Case_Function() + { + try + { + //Arrange + Setup(); + + var actionManifest = new ActionManifestManager(); + actionManifest.Initialize(_hc); + + _ec.Object.ExpressionValues["github"] = new LegacyContextData.DictionaryContextData + { + { "ref", new LegacyContextData.StringContextData("refs/heads/main") }, + }; + _ec.Object.ExpressionValues["strategy"] = new LegacyContextData.DictionaryContextData(); + _ec.Object.ExpressionValues["matrix"] = new LegacyContextData.DictionaryContextData(); + _ec.Object.ExpressionValues["steps"] = new LegacyContextData.DictionaryContextData(); + _ec.Object.ExpressionValues["job"] = new LegacyContextData.DictionaryContextData(); + _ec.Object.ExpressionValues["runner"] = new LegacyContextData.DictionaryContextData(); + _ec.Object.ExpressionValues["env"] = new LegacyContextData.DictionaryContextData(); + _ec.Object.ExpressionFunctions.Add(new LegacyExpressions.FunctionInfo("hashFiles", 1, 255)); + + // Act — evaluate a case() expression as a default input value. + // The feature flag is set, so this should succeed. + var token = new BasicExpressionToken(null, null, null, "case(true, 'matched', 'default')"); + var result = actionManifest.EvaluateDefaultInput(_ec.Object, "testInput", token); + + // Assert — case() should evaluate successfully + Assert.Equal("matched", result); + } + finally + { + Teardown(); + } + } + private void Teardown() { _hc?.Dispose(); From b9275b59cf592b1848899bfe8778b5abf567bd8c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:58:17 +0000 Subject: [PATCH 05/15] chore: update Node versions (#4319) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- src/Misc/externals.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Misc/externals.sh b/src/Misc/externals.sh index 874c39394..abd37f1de 100755 --- a/src/Misc/externals.sh +++ b/src/Misc/externals.sh @@ -6,8 +6,8 @@ NODE_URL=https://nodejs.org/dist 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.1" -NODE24_VERSION="24.14.0" +NODE20_VERSION="20.20.2" +NODE24_VERSION="24.14.1" get_abs_path() { # exploits the fact that pwd will print abs path when no args From 7ff994b93207d7a19bb28e03ce359d7f20dd2352 Mon Sep 17 00:00:00 2001 From: Stefan Penner Date: Tue, 31 Mar 2026 11:28:43 -0600 Subject: [PATCH 06/15] Batch and deduplicate action resolution across composite depths (#4296) Co-authored-by: Stefan Penner Co-authored-by: Claude Opus 4.6 --- src/Runner.Common/Constants.cs | 1 + src/Runner.Worker/ActionManager.cs | 227 ++++++++- src/Test/L0/Worker/ActionManagerL0.cs | 654 ++++++++++++++++++++++++++ 3 files changed, 879 insertions(+), 3 deletions(-) diff --git a/src/Runner.Common/Constants.cs b/src/Runner.Common/Constants.cs index 3326e947d..a6102ee97 100644 --- a/src/Runner.Common/Constants.cs +++ b/src/Runner.Common/Constants.cs @@ -177,6 +177,7 @@ namespace GitHub.Runner.Common public static readonly string SetOrchestrationIdEnvForActions = "actions_set_orchestration_id_env_for_actions"; public static readonly string SendJobLevelAnnotations = "actions_send_job_level_annotations"; public static readonly string EmitCompositeMarkers = "actions_runner_emit_composite_markers"; + public static readonly string BatchActionResolution = "actions_batch_action_resolution"; } // Node version migration related constants diff --git a/src/Runner.Worker/ActionManager.cs b/src/Runner.Worker/ActionManager.cs index 6c066a150..896396cfa 100644 --- a/src/Runner.Worker/ActionManager.cs +++ b/src/Runner.Worker/ActionManager.cs @@ -79,6 +79,13 @@ namespace GitHub.Runner.Worker PreStepTracker = new Dictionary() }; var containerSetupSteps = new List(); + var batchActionResolution = (executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.BatchActionResolution) ?? false) + || StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION")); + // Stack-local cache: same action (owner/repo@ref) is resolved only once, + // even if it appears at multiple depths in a composite tree. + var resolvedDownloadInfos = batchActionResolution + ? new Dictionary(StringComparer.Ordinal) + : null; var depth = 0; // We are running at the start of a job if (rootStepId == default(Guid)) @@ -105,7 +112,9 @@ namespace GitHub.Runner.Worker PrepareActionsState result = new PrepareActionsState(); try { - result = await PrepareActionsRecursiveAsync(executionContext, state, actions, depth, rootStepId); + result = batchActionResolution + ? await PrepareActionsRecursiveAsync(executionContext, state, actions, resolvedDownloadInfos, depth, rootStepId) + : await PrepareActionsRecursiveLegacyAsync(executionContext, state, actions, depth, rootStepId); } catch (FailedToResolveActionDownloadInfoException ex) { @@ -169,7 +178,192 @@ namespace GitHub.Runner.Worker return new PrepareResult(containerSetupSteps, result.PreStepTracker); } - private async Task PrepareActionsRecursiveAsync(IExecutionContext executionContext, PrepareActionsState state, IEnumerable actions, Int32 depth = 0, Guid parentStepId = default(Guid)) + private async Task PrepareActionsRecursiveAsync(IExecutionContext executionContext, PrepareActionsState state, IEnumerable actions, Dictionary resolvedDownloadInfos, Int32 depth = 0, Guid parentStepId = default(Guid)) + { + ArgUtil.NotNull(executionContext, nameof(executionContext)); + if (depth > Constants.CompositeActionsMaxDepth) + { + throw new Exception($"Composite action depth exceeded max depth {Constants.CompositeActionsMaxDepth}"); + } + + var repositoryActions = new List(); + + foreach (var action in actions) + { + if (action.Reference.Type == Pipelines.ActionSourceType.ContainerRegistry) + { + ArgUtil.NotNull(action, nameof(action)); + var containerReference = action.Reference as Pipelines.ContainerRegistryReference; + ArgUtil.NotNull(containerReference, nameof(containerReference)); + ArgUtil.NotNullOrEmpty(containerReference.Image, nameof(containerReference.Image)); + + if (!state.ImagesToPull.ContainsKey(containerReference.Image)) + { + state.ImagesToPull[containerReference.Image] = new List(); + } + + Trace.Info($"Action {action.Name} ({action.Id}) needs to pull image '{containerReference.Image}'"); + state.ImagesToPull[containerReference.Image].Add(action.Id); + } + else if (action.Reference.Type == Pipelines.ActionSourceType.Repository) + { + repositoryActions.Add(action); + } + } + + if (repositoryActions.Count > 0) + { + // Resolve download info, skipping any actions already cached. + await ResolveNewActionsAsync(executionContext, repositoryActions, resolvedDownloadInfos); + + // Download each action. + foreach (var action in repositoryActions) + { + var lookupKey = GetDownloadInfoLookupKey(action); + if (string.IsNullOrEmpty(lookupKey)) + { + continue; + } + if (!resolvedDownloadInfos.TryGetValue(lookupKey, out var downloadInfo)) + { + throw new Exception($"Missing download info for {lookupKey}"); + } + await DownloadRepositoryActionAsync(executionContext, downloadInfo); + } + + // Parse action.yml and collect composite sub-actions for batched + // resolution below. Pre/post step registration is deferred until + // after recursion so that HasPre/HasPost reflect the full subtree. + var nextLevel = new List<(Pipelines.ActionStep action, Guid parentId)>(); + + foreach (var action in repositoryActions) + { + var setupInfo = PrepareRepositoryActionAsync(executionContext, action); + if (setupInfo != null && setupInfo.Container != null) + { + if (!string.IsNullOrEmpty(setupInfo.Container.Image)) + { + if (!state.ImagesToPull.ContainsKey(setupInfo.Container.Image)) + { + state.ImagesToPull[setupInfo.Container.Image] = new List(); + } + + Trace.Info($"Action {action.Name} ({action.Id}) from repository '{setupInfo.Container.ActionRepository}' needs to pull image '{setupInfo.Container.Image}'"); + state.ImagesToPull[setupInfo.Container.Image].Add(action.Id); + } + else + { + ArgUtil.NotNullOrEmpty(setupInfo.Container.ActionRepository, nameof(setupInfo.Container.ActionRepository)); + + if (!state.ImagesToBuild.ContainsKey(setupInfo.Container.ActionRepository)) + { + state.ImagesToBuild[setupInfo.Container.ActionRepository] = new List(); + } + + Trace.Info($"Action {action.Name} ({action.Id}) from repository '{setupInfo.Container.ActionRepository}' needs to build image '{setupInfo.Container.Dockerfile}'"); + state.ImagesToBuild[setupInfo.Container.ActionRepository].Add(action.Id); + state.ImagesToBuildInfo[setupInfo.Container.ActionRepository] = setupInfo.Container; + } + } + else if (setupInfo != null && setupInfo.Steps != null && setupInfo.Steps.Count > 0) + { + foreach (var step in setupInfo.Steps) + { + nextLevel.Add((step, action.Id)); + } + } + } + + // Resolve all next-level sub-actions in one batch API call, + // then recurse per parent (which hits the cache, not the API). + if (nextLevel.Count > 0) + { + var nextLevelRepoActions = nextLevel + .Where(x => x.action.Reference.Type == Pipelines.ActionSourceType.Repository) + .Select(x => x.action) + .ToList(); + await ResolveNewActionsAsync(executionContext, nextLevelRepoActions, resolvedDownloadInfos); + + foreach (var group in nextLevel.GroupBy(x => x.parentId)) + { + var groupActions = group.Select(x => x.action).ToList(); + state = await PrepareActionsRecursiveAsync(executionContext, state, groupActions, resolvedDownloadInfos, depth + 1, group.Key); + } + } + + // Register pre/post steps after recursion so that HasPre/HasPost + // are correct (they depend on _cachedEmbeddedPreSteps/PostSteps + // being populated by the recursive calls above). + foreach (var action in repositoryActions) + { + var repoAction = action.Reference as Pipelines.RepositoryPathReference; + if (repoAction.RepositoryType != Pipelines.PipelineConstants.SelfAlias) + { + var definition = LoadAction(executionContext, action); + if (definition.Data.Execution.HasPre) + { + Trace.Info($"Add 'pre' execution for {action.Id}"); + // Root Step + if (depth < 1) + { + var actionRunner = HostContext.CreateService(); + actionRunner.Action = action; + actionRunner.Stage = ActionRunStage.Pre; + actionRunner.Condition = definition.Data.Execution.InitCondition; + state.PreStepTracker[action.Id] = actionRunner; + } + // Embedded Step + else + { + if (!_cachedEmbeddedPreSteps.ContainsKey(parentStepId)) + { + _cachedEmbeddedPreSteps[parentStepId] = new List(); + } + // Clone action so we can modify the condition without affecting the original + var clonedAction = action.Clone() as Pipelines.ActionStep; + clonedAction.Condition = definition.Data.Execution.InitCondition; + _cachedEmbeddedPreSteps[parentStepId].Add(clonedAction); + } + } + + if (definition.Data.Execution.HasPost && depth > 0) + { + if (!_cachedEmbeddedPostSteps.ContainsKey(parentStepId)) + { + // If we haven't done so already, add the parent to the post steps + _cachedEmbeddedPostSteps[parentStepId] = new Stack(); + } + // Clone action so we can modify the condition without affecting the original + var clonedAction = action.Clone() as Pipelines.ActionStep; + clonedAction.Condition = definition.Data.Execution.CleanupCondition; + _cachedEmbeddedPostSteps[parentStepId].Push(clonedAction); + } + } + else if (depth > 0) + { + // if we're in a composite action and haven't loaded the local action yet + // we assume it has a post step + if (!_cachedEmbeddedPostSteps.ContainsKey(parentStepId)) + { + // If we haven't done so already, add the parent to the post steps + _cachedEmbeddedPostSteps[parentStepId] = new Stack(); + } + // Clone action so we can modify the condition without affecting the original + var clonedAction = action.Clone() as Pipelines.ActionStep; + _cachedEmbeddedPostSteps[parentStepId].Push(clonedAction); + } + } + } + + return state; + } + + /// + /// Legacy (non-batched) action resolution. Each composite resolves its + /// sub-actions individually, with no cross-depth deduplication. + /// Used when the BatchActionResolution feature flag is disabled. + /// + private async Task PrepareActionsRecursiveLegacyAsync(IExecutionContext executionContext, PrepareActionsState state, IEnumerable actions, Int32 depth = 0, Guid parentStepId = default(Guid)) { ArgUtil.NotNull(executionContext, nameof(executionContext)); if (depth > Constants.CompositeActionsMaxDepth) @@ -255,7 +449,7 @@ namespace GitHub.Runner.Worker } else if (setupInfo != null && setupInfo.Steps != null && setupInfo.Steps.Count > 0) { - state = await PrepareActionsRecursiveAsync(executionContext, state, setupInfo.Steps, depth + 1, action.Id); + state = await PrepareActionsRecursiveLegacyAsync(executionContext, state, setupInfo.Steps, depth + 1, action.Id); } var repoAction = action.Reference as Pipelines.RepositoryPathReference; if (repoAction.RepositoryType != Pipelines.PipelineConstants.SelfAlias) @@ -762,6 +956,33 @@ namespace GitHub.Runner.Worker return actionDownloadInfos.Actions; } + /// + /// Only resolves actions not already in resolvedDownloadInfos. + /// Results are cached for reuse at deeper recursion levels. + /// + private async Task ResolveNewActionsAsync(IExecutionContext executionContext, List actions, Dictionary resolvedDownloadInfos) + { + var actionsToResolve = new List(); + var pendingKeys = new HashSet(StringComparer.Ordinal); + foreach (var action in actions) + { + var lookupKey = GetDownloadInfoLookupKey(action); + if (!string.IsNullOrEmpty(lookupKey) && !resolvedDownloadInfos.ContainsKey(lookupKey) && pendingKeys.Add(lookupKey)) + { + actionsToResolve.Add(action); + } + } + + if (actionsToResolve.Count > 0) + { + var downloadInfos = await GetDownloadInfoAsync(executionContext, actionsToResolve); + foreach (var kvp in downloadInfos) + { + resolvedDownloadInfos[kvp.Key] = kvp.Value; + } + } + } + private async Task DownloadRepositoryActionAsync(IExecutionContext executionContext, WebApi.ActionDownloadInfo downloadInfo) { Trace.Entering(); diff --git a/src/Test/L0/Worker/ActionManagerL0.cs b/src/Test/L0/Worker/ActionManagerL0.cs index c16dd55ae..501402a88 100644 --- a/src/Test/L0/Worker/ActionManagerL0.cs +++ b/src/Test/L0/Worker/ActionManagerL0.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.IO; using System.IO.Compression; using System.Net; @@ -1254,6 +1255,659 @@ runs: } #endif + // ================================================================= + // Tests for batched action resolution optimization + // ================================================================= + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_BatchesResolutionAcrossCompositeActions() + { + // Verifies that when multiple composite actions at the same depth + // reference sub-actions, those sub-actions are resolved in a single + // batched API call rather than one call per composite. + // + // Action tree: + // CompositePrestep (composite) → [Node action, CompositePrestep2 (composite)] + // CompositePrestep2 (composite) → [Node action, Docker action] + // + // Without batching: 3 API calls (depth 0, depth 1 for CompositePrestep, depth 2 for CompositePrestep2) + // With batching: still 3 calls at most, but the key is that depth-1 + // sub-actions from all composites at depth 0 are batched into 1 call. + // And the same action appearing at multiple depths triggers only 1 resolve. + Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", "true"); + try + { + //Arrange + Setup(); + _hc.EnqueueInstance(new Mock().Object); + _hc.EnqueueInstance(new Mock().Object); + _hc.EnqueueInstance(new Mock().Object); + + var resolveCallCount = 0; + var resolvedActions = new List(); + _jobServer.Setup(x => x.ResolveActionDownloadInfoAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((Guid scopeIdentifier, string hubName, Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken) => + { + resolveCallCount++; + resolvedActions.Add(actions); + var result = new ActionDownloadInfoCollection { Actions = new Dictionary() }; + foreach (var action in actions.Actions) + { + var key = $"{action.NameWithOwner}@{action.Ref}"; + result.Actions[key] = new ActionDownloadInfo + { + NameWithOwner = action.NameWithOwner, + Ref = action.Ref, + ResolvedNameWithOwner = action.NameWithOwner, + ResolvedSha = $"{action.Ref}-sha", + TarballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/tarball/{action.Ref}", + ZipballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/zipball/{action.Ref}", + }; + } + return Task.FromResult(result); + }); + + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "CompositePrestep", + RepositoryType = "GitHub" + } + } + }; + + //Act + var result = await _actionManager.PrepareActionsAsync(_ec.Object, actions); + + //Assert + // The composite tree is: + // depth 0: CompositePrestep + // depth 1: Node@RepositoryActionWithWrapperActionfile_Node + CompositePrestep2 + // depth 2: Node@RepositoryActionWithWrapperActionfile_Node + Docker@RepositoryActionWithWrapperActionfile_Docker + // + // With batching: + // Call 1 (depth 0, resolve): CompositePrestep + // Call 2 (depth 0→1, pre-resolve): Node + CompositePrestep2 in one batch + // Call 3 (depth 1→2, pre-resolve): Docker only (Node already cached from call 2) + Assert.Equal(3, resolveCallCount); + + // Call 1: depth 0 resolve — just the top-level composite + var call1Keys = resolvedActions[0].Actions.Select(a => $"{a.NameWithOwner}@{a.Ref}").OrderBy(k => k).ToList(); + Assert.Equal(new[] { "TingluoHuang/runner_L0@CompositePrestep" }, call1Keys); + + // Call 2: depth 0→1 pre-resolve — batch both children of CompositePrestep + var call2Keys = resolvedActions[1].Actions.Select(a => $"{a.NameWithOwner}@{a.Ref}").OrderBy(k => k).ToList(); + Assert.Equal(new[] { "TingluoHuang/runner_L0@CompositePrestep2", "TingluoHuang/runner_L0@RepositoryActionWithWrapperActionfile_Node" }, call2Keys); + + // Call 3: depth 1→2 pre-resolve — only Docker (Node was cached in call 2) + var call3Keys = resolvedActions[2].Actions.Select(a => $"{a.NameWithOwner}@{a.Ref}").OrderBy(k => k).ToList(); + Assert.Equal(new[] { "TingluoHuang/runner_L0@RepositoryActionWithWrapperActionfile_Docker" }, call3Keys); + + // Verify all actions were downloaded + Assert.True(File.Exists(Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang/runner_L0", "CompositePrestep.completed"))); + Assert.True(File.Exists(Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang/runner_L0", "RepositoryActionWithWrapperActionfile_Node.completed"))); + Assert.True(File.Exists(Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang/runner_L0", "CompositePrestep2.completed"))); + Assert.True(File.Exists(Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang/runner_L0", "RepositoryActionWithWrapperActionfile_Docker.completed"))); + + // Verify pre-step tracking still works correctly + Assert.Equal(1, result.PreStepTracker.Count); + } + finally + { + Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", null); + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_DeduplicatesResolutionAcrossDepthLevels() + { + // Verifies that an action appearing at multiple depths in the + // composite tree is only resolved once (not re-resolved at each level). + // + // CompositePrestep uses Node action at depth 1. + // CompositePrestep2 (also at depth 1) uses the SAME Node action at depth 2. + // The Node action should only be resolved once total. + Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", "true"); + try + { + //Arrange + Setup(); + _hc.EnqueueInstance(new Mock().Object); + _hc.EnqueueInstance(new Mock().Object); + _hc.EnqueueInstance(new Mock().Object); + + var allResolvedKeys = new List(); + _jobServer.Setup(x => x.ResolveActionDownloadInfoAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((Guid scopeIdentifier, string hubName, Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken) => + { + var result = new ActionDownloadInfoCollection { Actions = new Dictionary() }; + foreach (var action in actions.Actions) + { + var key = $"{action.NameWithOwner}@{action.Ref}"; + allResolvedKeys.Add(key); + result.Actions[key] = new ActionDownloadInfo + { + NameWithOwner = action.NameWithOwner, + Ref = action.Ref, + ResolvedNameWithOwner = action.NameWithOwner, + ResolvedSha = $"{action.Ref}-sha", + TarballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/tarball/{action.Ref}", + ZipballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/zipball/{action.Ref}", + }; + } + return Task.FromResult(result); + }); + + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "CompositePrestep", + RepositoryType = "GitHub" + } + } + }; + + //Act + await _actionManager.PrepareActionsAsync(_ec.Object, actions); + + //Assert + // TingluoHuang/runner_L0@RepositoryActionWithWrapperActionfile_Node appears + // at both depth 1 (sub-step of CompositePrestep) and depth 2 (sub-step of + // CompositePrestep2). With deduplication it should only be resolved once. + var nodeActionKey = "TingluoHuang/runner_L0@RepositoryActionWithWrapperActionfile_Node"; + var nodeResolveCount = allResolvedKeys.FindAll(k => k == nodeActionKey).Count; + Assert.Equal(1, nodeResolveCount); + + // Verify the total number of unique actions resolved matches the tree + var uniqueKeys = new HashSet(allResolvedKeys); + // Expected unique actions: CompositePrestep, Node, CompositePrestep2, Docker = 4 + Assert.Equal(4, uniqueKeys.Count); + } + finally + { + Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", null); + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_MultipleTopLevelActions_BatchesResolution() + { + // Verifies that multiple independent actions at depth 0 are + // resolved in a single API call. + Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", "true"); + try + { + //Arrange + Setup(); + // Node action has pre+post, needs IActionRunner instances + _hc.EnqueueInstance(new Mock().Object); + _hc.EnqueueInstance(new Mock().Object); + + var resolveCallCount = 0; + var firstCallActionCount = 0; + _jobServer.Setup(x => x.ResolveActionDownloadInfoAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((Guid scopeIdentifier, string hubName, Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken) => + { + resolveCallCount++; + if (resolveCallCount == 1) + { + firstCallActionCount = actions.Actions.Count; + } + var result = new ActionDownloadInfoCollection { Actions = new Dictionary() }; + foreach (var action in actions.Actions) + { + var key = $"{action.NameWithOwner}@{action.Ref}"; + result.Actions[key] = new ActionDownloadInfo + { + NameWithOwner = action.NameWithOwner, + Ref = action.Ref, + ResolvedNameWithOwner = action.NameWithOwner, + ResolvedSha = $"{action.Ref}-sha", + TarballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/tarball/{action.Ref}", + ZipballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/zipball/{action.Ref}", + }; + } + return Task.FromResult(result); + }); + + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action1", + Id = Guid.NewGuid(), + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "RepositoryActionWithWrapperActionfile_Node", + RepositoryType = "GitHub" + } + }, + new Pipelines.ActionStep() + { + Name = "action2", + Id = Guid.NewGuid(), + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "RepositoryActionWithWrapperActionfile_Docker", + RepositoryType = "GitHub" + } + } + }; + + //Act + await _actionManager.PrepareActionsAsync(_ec.Object, actions); + + //Assert + // Both actions are at depth 0 — should be resolved in a single batch call + Assert.Equal(1, resolveCallCount); + Assert.Equal(2, firstCallActionCount); + + // Verify both were downloaded + Assert.True(File.Exists(Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang/runner_L0", "RepositoryActionWithWrapperActionfile_Node.completed"))); + Assert.True(File.Exists(Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang/runner_L0", "RepositoryActionWithWrapperActionfile_Docker.completed"))); + } + finally + { + Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", null); + Teardown(); + } + } + +#if OS_LINUX + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_NestedCompositeContainers_BatchedResolution() + { + // Verifies batching with nested composite actions that reference + // container actions (Linux-only since containers require Linux). + // + // CompositeContainerNested (composite): + // → repositoryactionwithdockerfile (Dockerfile) + // → CompositeContainerNested2 (composite): + // → repositoryactionwithdockerfile (Dockerfile, same as above) + // → notpullorbuildimagesmultipletimes1 (Dockerfile) + Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", "true"); + try + { + //Arrange + Setup(); + + var resolveCallCount = 0; + _jobServer.Setup(x => x.ResolveActionDownloadInfoAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((Guid scopeIdentifier, string hubName, Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken) => + { + resolveCallCount++; + var result = new ActionDownloadInfoCollection { Actions = new Dictionary() }; + foreach (var action in actions.Actions) + { + var key = $"{action.NameWithOwner}@{action.Ref}"; + result.Actions[key] = new ActionDownloadInfo + { + NameWithOwner = action.NameWithOwner, + Ref = action.Ref, + ResolvedNameWithOwner = action.NameWithOwner, + ResolvedSha = $"{action.Ref}-sha", + TarballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/tarball/{action.Ref}", + ZipballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/zipball/{action.Ref}", + }; + } + return Task.FromResult(result); + }); + + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "CompositeContainerNested", + RepositoryType = "GitHub" + } + } + }; + + //Act + var result = await _actionManager.PrepareActionsAsync(_ec.Object, actions); + + //Assert + // Tree has 3 depth levels with 5 unique actions. + // With batching, should need at most 3 resolve calls (one per depth level). + Assert.True(resolveCallCount <= 3, $"Expected at most 3 resolve calls but got {resolveCallCount}"); + + // repositoryactionwithdockerfile appears at both depth 1 and depth 2. + // Container setup should still work correctly — 2 unique Docker images. + Assert.Equal(2, result.ContainerSetupSteps.Count); + } + finally + { + Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", null); + Teardown(); + } + } +#endif + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_ParallelDownloads_MultipleUniqueActions() + { + // Verifies that multiple unique top-level actions are downloaded via + // DownloadActionsInParallelAsync (the parallel code path), and that + // all actions are correctly resolved and downloaded. + Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", "true"); + try + { + //Arrange + Setup(); + // Node action has pre step, and CompositePrestep recurses into + // sub-actions that also need IActionRunner instances + _hc.EnqueueInstance(new Mock().Object); + _hc.EnqueueInstance(new Mock().Object); + _hc.EnqueueInstance(new Mock().Object); + _hc.EnqueueInstance(new Mock().Object); + _hc.EnqueueInstance(new Mock().Object); + + var resolveCallCount = 0; + _jobServer.Setup(x => x.ResolveActionDownloadInfoAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((Guid scopeIdentifier, string hubName, Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken) => + { + Interlocked.Increment(ref resolveCallCount); + var result = new ActionDownloadInfoCollection { Actions = new Dictionary() }; + foreach (var action in actions.Actions) + { + var key = $"{action.NameWithOwner}@{action.Ref}"; + result.Actions[key] = new ActionDownloadInfo + { + NameWithOwner = action.NameWithOwner, + Ref = action.Ref, + ResolvedNameWithOwner = action.NameWithOwner, + ResolvedSha = $"{action.Ref}-sha", + TarballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/tarball/{action.Ref}", + ZipballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/zipball/{action.Ref}", + }; + } + return Task.FromResult(result); + }); + + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action1", + Id = Guid.NewGuid(), + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "RepositoryActionWithWrapperActionfile_Node", + RepositoryType = "GitHub" + } + }, + new Pipelines.ActionStep() + { + Name = "action2", + Id = Guid.NewGuid(), + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "RepositoryActionWithWrapperActionfile_Docker", + RepositoryType = "GitHub" + } + }, + new Pipelines.ActionStep() + { + Name = "action3", + Id = Guid.NewGuid(), + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "CompositePrestep", + RepositoryType = "GitHub" + } + } + }; + + //Act + await _actionManager.PrepareActionsAsync(_ec.Object, actions); + + //Assert + // 3 unique actions at depth 0 → triggers DownloadActionsInParallelAsync + // (parallel path used when uniqueDownloads.Count > 1) + var nodeCompleted = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang/runner_L0", "RepositoryActionWithWrapperActionfile_Node.completed"); + var dockerCompleted = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang/runner_L0", "RepositoryActionWithWrapperActionfile_Docker.completed"); + var compositeCompleted = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Actions), "TingluoHuang/runner_L0", "CompositePrestep.completed"); + + Assert.True(File.Exists(nodeCompleted), $"Expected watermark at {nodeCompleted}"); + Assert.True(File.Exists(dockerCompleted), $"Expected watermark at {dockerCompleted}"); + Assert.True(File.Exists(compositeCompleted), $"Expected watermark at {compositeCompleted}"); + + // All depth-0 actions resolved in a single batch call. + // Composite sub-actions may add 1-2 more calls. + Assert.True(resolveCallCount >= 1, "Expected at least 1 resolve call"); + } + finally + { + Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", null); + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_DownloadsNextLevelActionsBeforeRecursing() + { + // Verifies that depth-1 actions are downloaded before the depth-2 + // pre-resolve fires. We detect this by snapshotting watermark state + // inside the 3rd ResolveActionDownloadInfoAsync callback (which is + // the depth-2 pre-resolve). If pre-download works, depth-1 watermarks + // already exist at that point. + // + // Action tree: + // CompositePrestep (composite) → [Node, CompositePrestep2 (composite)] + // CompositePrestep2 (composite) → [Node, Docker] + // + // Without pre-download: downloads happen during recursion (serial per depth) + // With pre-download: depth 1 actions (Node + CompositePrestep2) are + // downloaded in parallel before recursing, so recursion is a no-op + // for downloads. + Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", "true"); + try + { + //Arrange + Setup(); + _hc.EnqueueInstance(new Mock().Object); + _hc.EnqueueInstance(new Mock().Object); + _hc.EnqueueInstance(new Mock().Object); + + // Track watermark state at the time of each resolve call. + // If pre-download works, when the 3rd resolve fires (depth 2 + // pre-resolve for Docker), the depth-1 actions (Node + + // CompositePrestep2) should already have watermarks on disk. + var resolveCallCount = 0; + var watermarksAtResolve3 = new Dictionary(); + _jobServer.Setup(x => x.ResolveActionDownloadInfoAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((Guid scopeIdentifier, string hubName, Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken) => + { + resolveCallCount++; + if (resolveCallCount == 3) + { + // At the time of the 3rd resolve, check if depth-1 actions + // are already downloaded (pre-download should have done this) + var actionsDir2 = _hc.GetDirectory(WellKnownDirectory.Actions); + watermarksAtResolve3["Node"] = File.Exists(Path.Combine(actionsDir2, "TingluoHuang/runner_L0", "RepositoryActionWithWrapperActionfile_Node.completed")); + watermarksAtResolve3["CompositePrestep2"] = File.Exists(Path.Combine(actionsDir2, "TingluoHuang/runner_L0", "CompositePrestep2.completed")); + } + var result = new ActionDownloadInfoCollection { Actions = new Dictionary() }; + foreach (var action in actions.Actions) + { + var key = $"{action.NameWithOwner}@{action.Ref}"; + result.Actions[key] = new ActionDownloadInfo + { + NameWithOwner = action.NameWithOwner, + Ref = action.Ref, + ResolvedNameWithOwner = action.NameWithOwner, + ResolvedSha = $"{action.Ref}-sha", + TarballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/tarball/{action.Ref}", + ZipballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/zipball/{action.Ref}", + }; + } + return Task.FromResult(result); + }); + + var actionId = Guid.NewGuid(); + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action", + Id = actionId, + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "CompositePrestep", + RepositoryType = "GitHub" + } + } + }; + + //Act + var result = await _actionManager.PrepareActionsAsync(_ec.Object, actions); + + //Assert + // All actions should be downloaded (watermarks exist) + var actionsDir = _hc.GetDirectory(WellKnownDirectory.Actions); + Assert.True(File.Exists(Path.Combine(actionsDir, "TingluoHuang/runner_L0", "CompositePrestep.completed"))); + Assert.True(File.Exists(Path.Combine(actionsDir, "TingluoHuang/runner_L0", "RepositoryActionWithWrapperActionfile_Node.completed"))); + Assert.True(File.Exists(Path.Combine(actionsDir, "TingluoHuang/runner_L0", "CompositePrestep2.completed"))); + Assert.True(File.Exists(Path.Combine(actionsDir, "TingluoHuang/runner_L0", "RepositoryActionWithWrapperActionfile_Docker.completed"))); + + // 3 resolve calls total + Assert.Equal(3, resolveCallCount); + + // The key assertion: at the time of the 3rd resolve call + // (pre-resolve for depth 2), the depth-1 actions should + // ALREADY be downloaded thanks to pre-download. + // Without pre-download, these watermarks wouldn't exist yet + // because depth-1 downloads would only happen during recursion. + Assert.True(watermarksAtResolve3["Node"], + "Node action should be pre-downloaded before depth 2 pre-resolve"); + Assert.True(watermarksAtResolve3["CompositePrestep2"], + "CompositePrestep2 should be pre-downloaded before depth 2 pre-resolve"); + } + finally + { + Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", null); + Teardown(); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public async void PrepareActions_ParallelDownloadsAtSameDepth() + { + // Verifies that multiple unique actions at the same depth are + // downloaded concurrently (Task.WhenAll) rather than sequentially. + // We detect this by checking that all watermarks exist after a + // single PrepareActionsAsync call with multiple top-level actions. + Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", "true"); + try + { + //Arrange + Setup(); + _hc.EnqueueInstance(new Mock().Object); + _hc.EnqueueInstance(new Mock().Object); + + _jobServer.Setup(x => x.ResolveActionDownloadInfoAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Returns((Guid scopeIdentifier, string hubName, Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken) => + { + var result = new ActionDownloadInfoCollection { Actions = new Dictionary() }; + foreach (var action in actions.Actions) + { + var key = $"{action.NameWithOwner}@{action.Ref}"; + result.Actions[key] = new ActionDownloadInfo + { + NameWithOwner = action.NameWithOwner, + Ref = action.Ref, + ResolvedNameWithOwner = action.NameWithOwner, + ResolvedSha = $"{action.Ref}-sha", + TarballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/tarball/{action.Ref}", + ZipballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/zipball/{action.Ref}", + }; + } + return Task.FromResult(result); + }); + + var actions = new List + { + new Pipelines.ActionStep() + { + Name = "action1", + Id = Guid.NewGuid(), + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "RepositoryActionWithWrapperActionfile_Node", + RepositoryType = "GitHub" + } + }, + new Pipelines.ActionStep() + { + Name = "action2", + Id = Guid.NewGuid(), + Reference = new Pipelines.RepositoryPathReference() + { + Name = "TingluoHuang/runner_L0", + Ref = "RepositoryActionWithWrapperActionfile_Docker", + RepositoryType = "GitHub" + } + } + }; + + //Act + await _actionManager.PrepareActionsAsync(_ec.Object, actions); + + //Assert - both downloaded (parallel path used when > 1 unique download) + var actionsDir = _hc.GetDirectory(WellKnownDirectory.Actions); + Assert.True(File.Exists(Path.Combine(actionsDir, "TingluoHuang/runner_L0", "RepositoryActionWithWrapperActionfile_Node.completed"))); + Assert.True(File.Exists(Path.Combine(actionsDir, "TingluoHuang/runner_L0", "RepositoryActionWithWrapperActionfile_Docker.completed"))); + } + finally + { + Environment.SetEnvironmentVariable("ACTIONS_BATCH_ACTION_RESOLUTION", null); + Teardown(); + } + } + [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] From 5c6dd47e762e7f10a9c52bfcd9af796d9802993a Mon Sep 17 00:00:00 2001 From: Tingluo Huang Date: Tue, 31 Mar 2026 17:51:01 -0400 Subject: [PATCH 07/15] Add support for Bearer token in action archive downloads (#4321) --- src/Runner.Common/Constants.cs | 1 + src/Runner.Worker/ActionManager.cs | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/Runner.Common/Constants.cs b/src/Runner.Common/Constants.cs index a6102ee97..7a1f1cbb0 100644 --- a/src/Runner.Common/Constants.cs +++ b/src/Runner.Common/Constants.cs @@ -178,6 +178,7 @@ namespace GitHub.Runner.Common public static readonly string SendJobLevelAnnotations = "actions_send_job_level_annotations"; 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"; } // Node version migration related constants diff --git a/src/Runner.Worker/ActionManager.cs b/src/Runner.Worker/ActionManager.cs index 896396cfa..79c0de5f7 100644 --- a/src/Runner.Worker/ActionManager.cs +++ b/src/Runner.Worker/ActionManager.cs @@ -1367,16 +1367,29 @@ namespace GitHub.Runner.Worker return $"{repositoryReference.Name}@{repositoryReference.Ref}"; } - private AuthenticationHeaderValue CreateAuthHeader(string token) + private AuthenticationHeaderValue CreateAuthHeader(IExecutionContext executionContext, string downloadUrl, string token) { if (string.IsNullOrEmpty(token)) { return null; } - var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"x-access-token:{token}")); - HostContext.SecretMasker.AddValue(base64EncodingToken); - return new AuthenticationHeaderValue("Basic", base64EncodingToken); + if (executionContext.Global.Variables.GetBoolean(Constants.Runner.Features.UseBearerTokenForCodeload) == true && + Uri.TryCreate(downloadUrl, UriKind.Absolute, out var parsedUrl) && + !string.IsNullOrEmpty(parsedUrl?.Host) && + !string.IsNullOrEmpty(parsedUrl?.PathAndQuery) && + (parsedUrl.Host.StartsWith("codeload.", StringComparison.OrdinalIgnoreCase) || parsedUrl.PathAndQuery.StartsWith("/_codeload/", StringComparison.OrdinalIgnoreCase))) + { + Trace.Info("Using Bearer token for action archive download directly to codeload."); + return new AuthenticationHeaderValue("Bearer", token); + } + else + { + Trace.Info("Using Basic token for action archive download."); + var base64EncodingToken = Convert.ToBase64String(Encoding.UTF8.GetBytes($"x-access-token:{token}")); + HostContext.SecretMasker.AddValue(base64EncodingToken); + return new AuthenticationHeaderValue("Basic", base64EncodingToken); + } } private async Task DownloadRepositoryArchive(IExecutionContext executionContext, string downloadUrl, string downloadAuthToken, string archiveFile) @@ -1401,7 +1414,7 @@ namespace GitHub.Runner.Worker using (var httpClientHandler = HostContext.CreateHttpClientHandler()) using (var httpClient = new HttpClient(httpClientHandler)) { - httpClient.DefaultRequestHeaders.Authorization = CreateAuthHeader(downloadAuthToken); + httpClient.DefaultRequestHeaders.Authorization = CreateAuthHeader(executionContext, downloadUrl, downloadAuthToken); httpClient.DefaultRequestHeaders.UserAgent.AddRange(HostContext.UserAgents); using (var response = await httpClient.GetAsync(downloadUrl)) From df507886cb84e11f551a8a779a839ab36e41d0d8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:19:42 +0100 Subject: [PATCH 08/15] Bump brace-expansion in /src/Misc/expressionFunc/hashFiles (#4318) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Salman Chishti --- .../hashFiles/package-lock.json | 37 +++++++++---------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/Misc/expressionFunc/hashFiles/package-lock.json b/src/Misc/expressionFunc/hashFiles/package-lock.json index d8099aa52..da1aa8383 100644 --- a/src/Misc/expressionFunc/hashFiles/package-lock.json +++ b/src/Misc/expressionFunc/hashFiles/package-lock.json @@ -521,9 +521,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "dependencies": { "balanced-match": "^4.0.2" @@ -899,10 +899,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1771,9 +1770,9 @@ } }, "node_modules/eslint-plugin-github/node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "dependencies": { "balanced-match": "^4.0.2" @@ -5046,9 +5045,9 @@ "dev": true }, "brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "requires": { "balanced-match": "^4.0.2" @@ -5306,9 +5305,9 @@ } }, "brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5891,9 +5890,9 @@ "dev": true }, "brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "requires": { "balanced-match": "^4.0.2" From 7711dc53e2a760728e47e9e05941e756ed4529d5 Mon Sep 17 00:00:00 2001 From: Francesco Renzi Date: Tue, 7 Apr 2026 13:51:33 +0100 Subject: [PATCH 09/15] Add devtunnel connection for debugger jobs (#4317) --- src/Runner.Common/Tracing.cs | 7 + src/Runner.Worker/Dap/DapDebugger.cs | 192 ++++++-- src/Runner.Worker/Dap/DebuggerConfig.cs | 33 ++ src/Runner.Worker/ExecutionContext.cs | 2 +- src/Runner.Worker/GlobalContext.cs | 3 +- src/Runner.Worker/JobRunner.cs | 2 +- src/Runner.Worker/Runner.Worker.csproj | 1 + .../Pipelines/AgentJobRequestMessage.cs | 7 + .../Pipelines/DebuggerTunnelInfo.cs | 24 + .../Sdk/RSWebApi/AgentJobRequestMessageL0.cs | 50 ++ src/Test/L0/Worker/DapDebuggerL0.cs | 454 +++++++++--------- 11 files changed, 511 insertions(+), 264 deletions(-) create mode 100644 src/Runner.Worker/Dap/DebuggerConfig.cs create mode 100644 src/Sdk/DTPipelines/Pipelines/DebuggerTunnelInfo.cs diff --git a/src/Runner.Common/Tracing.cs b/src/Runner.Common/Tracing.cs index 3cc1d0393..9a2b035f6 100644 --- a/src/Runner.Common/Tracing.cs +++ b/src/Runner.Common/Tracing.cs @@ -12,6 +12,13 @@ namespace GitHub.Runner.Common private ISecretMasker _secretMasker; private TraceSource _traceSource; + /// + /// The underlying for this instance. + /// Useful when third-party libraries require a + /// to route their diagnostics into the runner's log infrastructure. + /// + public TraceSource Source => _traceSource; + public Tracing(string name, ISecretMasker secretMasker, SourceSwitch sourceSwitch, HostTraceListener traceListener, StdoutTraceListener stdoutTraceListener = null) { ArgUtil.NotNull(secretMasker, nameof(secretMasker)); diff --git a/src/Runner.Worker/Dap/DapDebugger.cs b/src/Runner.Worker/Dap/DapDebugger.cs index 9d0acca68..99b61e1b1 100644 --- a/src/Runner.Worker/Dap/DapDebugger.cs +++ b/src/Runner.Worker/Dap/DapDebugger.cs @@ -1,7 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; +using System.Linq; using System.Net; +using System.Net.Http.Headers; using System.Net.Sockets; using System.Text; using System.Threading; @@ -9,6 +12,9 @@ using System.Threading.Tasks; using GitHub.DistributedTask.WebApi; using GitHub.Runner.Common; using GitHub.Runner.Sdk; +using Microsoft.DevTunnels.Connections; +using Microsoft.DevTunnels.Contracts; +using Microsoft.DevTunnels.Management; using Newtonsoft.Json; namespace GitHub.Runner.Worker.Dap @@ -30,10 +36,10 @@ namespace GitHub.Runner.Worker.Dap /// public sealed class DapDebugger : RunnerService, IDapDebugger { - private const int _defaultPort = 4711; private const int _defaultTimeoutMinutes = 15; - private const string _portEnvironmentVariable = "ACTIONS_RUNNER_DAP_PORT"; private const string _timeoutEnvironmentVariable = "ACTIONS_RUNNER_DAP_CONNECTION_TIMEOUT"; + private const int _defaultTunnelConnectTimeoutSeconds = 30; + private const string _tunnelConnectTimeoutSeconds = "ACTIONS_RUNNER_DAP_TUNNEL_CONNECT_TIMEOUT_SECONDS"; private const string _contentLengthHeader = "Content-Length: "; private const int _maxMessageSize = 10 * 1024 * 1024; // 10 MB private const int _maxHeaderLineLength = 8192; // 8 KB @@ -58,6 +64,16 @@ namespace GitHub.Runner.Worker.Dap private CancellationTokenRegistration? _cancellationRegistration; private bool _isFirstStep = true; + // Dev Tunnel relay host for remote debugging + private TunnelRelayTunnelHost _tunnelRelayHost; + + // Cancellation source for the connection loop, cancelled in StopAsync + // so AcceptTcpClientAsync unblocks cleanly without relying on listener disposal. + private CancellationTokenSource _loopCts; + + // When true, skip tunnel relay startup (unit tests only) + internal bool SkipTunnelRelay { get; set; } + // Synchronization for step execution private TaskCompletionSource _commandTcs; private readonly object _stateLock = new object(); @@ -101,22 +117,38 @@ namespace GitHub.Runner.Worker.Dap Trace.Info("DapDebugger initialized"); } - public Task StartAsync(IExecutionContext jobContext) + public async Task StartAsync(IExecutionContext jobContext) { ArgUtil.NotNull(jobContext, nameof(jobContext)); - var port = ResolvePort(); + var debuggerConfig = jobContext.Global.Debugger; - Trace.Info($"Starting DAP debugger on port {port}"); + if (!debuggerConfig.HasValidTunnel) + { + throw new ArgumentException( + "Debugger requires valid tunnel configuration (tunnelId, clusterId, hostToken, port)."); + } + + Trace.Info($"Starting DAP debugger on port {debuggerConfig.Tunnel.Port}"); _jobContext = jobContext; _readyTcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - _listener = new TcpListener(IPAddress.Loopback, port); + _listener = new TcpListener(IPAddress.Loopback, debuggerConfig.Tunnel.Port); _listener.Start(); Trace.Info($"DAP debugger listening on {_listener.LocalEndpoint}"); + // Start Dev Tunnel relay so remote clients reach the local DAP port. + // The relay is torn down explicitly in StopAsync (after the DAP session + // is closed) so we do NOT pass the job cancellation token here — that + // would race with the DAP shutdown and drop the transport mid-protocol. + if (!SkipTunnelRelay) + { + await StartTunnelRelayAsync(debuggerConfig); + } + _state = DapSessionState.WaitingForConnection; - _connectionLoopTask = ConnectionLoopAsync(jobContext.CancellationToken); + _loopCts = CancellationTokenSource.CreateLinkedTokenSource(jobContext.CancellationToken); + _connectionLoopTask = ConnectionLoopAsync(_loopCts.Token); _cancellationRegistration = jobContext.CancellationToken.Register(() => { @@ -125,8 +157,44 @@ namespace GitHub.Runner.Worker.Dap _commandTcs?.TrySetResult(DapCommand.Disconnect); }); - Trace.Info($"DAP debugger started on port {port}"); - return Task.CompletedTask; + Trace.Info($"DAP debugger started on port {debuggerConfig.Tunnel.Port}"); + } + + private async Task StartTunnelRelayAsync(DebuggerConfig config) + { + Trace.Info($"Starting Dev Tunnel relay (tunnel={config.Tunnel.TunnelId}, cluster={config.Tunnel.ClusterId})"); + + var userAgents = HostContext.UserAgents.ToArray(); + var httpHandler = HostContext.CreateHttpClientHandler(); + httpHandler.AllowAutoRedirect = false; + + var managementClient = new TunnelManagementClient( + userAgents, + () => Task.FromResult(new AuthenticationHeaderValue("tunnel", config.Tunnel.HostToken)), + tunnelServiceUri: null, + httpHandler); + + var tunnel = new Tunnel + { + TunnelId = config.Tunnel.TunnelId, + ClusterId = config.Tunnel.ClusterId, + AccessTokens = new Dictionary + { + [TunnelAccessScopes.Host] = config.Tunnel.HostToken + }, + Ports = new[] + { + new TunnelPort { PortNumber = config.Tunnel.Port } + }, + }; + + _tunnelRelayHost = new TunnelRelayTunnelHost(managementClient, HostContext.GetTrace("DevTunnelRelay").Source); + var tunnelConnectTimeoutSeconds = ResolveTunnelConnectTimeout(); + using var connectCts = new CancellationTokenSource(TimeSpan.FromSeconds(tunnelConnectTimeoutSeconds)); + Trace.Info($"Connecting to Dev Tunnel relay (timeout: {tunnelConnectTimeoutSeconds}s)"); + await _tunnelRelayHost.ConnectAsync(tunnel, connectCts.Token); + + Trace.Info("Dev Tunnel relay started"); } public async Task WaitUntilReadyAsync() @@ -180,31 +248,55 @@ namespace GitHub.Runner.Worker.Dap _cancellationRegistration = null; } - if (_state != DapSessionState.NotStarted) + try { - try + if (_listener != null || _tunnelRelayHost != null || _connectionLoopTask != null) { Trace.Info("Stopping DAP debugger"); - - CleanupConnection(); - - try { _listener?.Stop(); } - catch { /* best effort */ } - - if (_connectionLoopTask != null) - { - try - { - await Task.WhenAny(_connectionLoopTask, Task.Delay(5000)); - } - catch { /* best effort */ } - } } - catch (Exception ex) + + // Tear down Dev Tunnel relay FIRST — it may hold connections to the + // local port and must be fully disposed before we release the listener, + // otherwise the next worker can't bind the same port. + if (_tunnelRelayHost != null) { - Trace.Error("Error stopping DAP debugger"); - Trace.Error(ex); + Trace.Info("Stopping Dev Tunnel relay"); + var disposeTask = _tunnelRelayHost.DisposeAsync().AsTask(); + if (await Task.WhenAny(disposeTask, Task.Delay(10_000)) != disposeTask) + { + Trace.Warning("Dev Tunnel relay dispose timed out after 10s"); + } + else + { + Trace.Info("Dev Tunnel relay stopped"); + } + + _tunnelRelayHost = null; } + + CleanupConnection(); + + // Cancel the connection loop first so AcceptTcpClientAsync unblocks + // cleanly, then stop the listener once nothing is using it. + try { _loopCts?.Cancel(); } + catch { /* best effort */ } + + try { _listener?.Stop(); } + catch { /* best effort */ } + + if (_connectionLoopTask != null) + { + try + { + await Task.WhenAny(_connectionLoopTask, Task.Delay(5000)); + } + catch { /* best effort */ } + } + } + catch (Exception ex) + { + Trace.Error("Error stopping DAP debugger"); + Trace.Error(ex); } lock (_stateLock) @@ -221,6 +313,8 @@ namespace GitHub.Runner.Worker.Dap _stream = null; _readyTcs = null; _connectionLoopTask = null; + _loopCts?.Dispose(); + _loopCts = null; } public async Task OnStepStartingAsync(IStep step) @@ -398,12 +492,7 @@ namespace GitHub.Runner.Worker.Dap try { Trace.Info("Waiting for debug client connection..."); - _client = await _listener.AcceptTcpClientAsync(); - - if (cancellationToken.IsCancellationRequested) - { - break; - } + _client = await _listener.AcceptTcpClientAsync(cancellationToken); _stream = _client.GetStream(); var remoteEndPoint = _client.Client.RemoteEndPoint; @@ -418,6 +507,10 @@ namespace GitHub.Runner.Worker.Dap HandleClientDisconnected(); CleanupConnection(); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + break; + } catch (Exception ex) { CleanupConnection(); @@ -427,6 +520,13 @@ namespace GitHub.Runner.Worker.Dap break; } + // If the listener has been stopped, don't retry. + if (_listener == null || !_listener.Server.IsBound) + { + Trace.Info("Listener stopped, exiting connection loop"); + break; + } + Trace.Error("Debugger connection error"); Trace.Error(ex); @@ -1272,18 +1372,6 @@ namespace GitHub.Runner.Worker.Dap }; } - internal int ResolvePort() - { - var portEnv = Environment.GetEnvironmentVariable(_portEnvironmentVariable); - if (!string.IsNullOrEmpty(portEnv) && int.TryParse(portEnv, out var customPort) && customPort > 1024 && customPort <= 65535) - { - Trace.Info($"Using custom DAP port {customPort} from {_portEnvironmentVariable}"); - return customPort; - } - - return _defaultPort; - } - internal int ResolveTimeout() { var timeoutEnv = Environment.GetEnvironmentVariable(_timeoutEnvironmentVariable); @@ -1295,5 +1383,17 @@ namespace GitHub.Runner.Worker.Dap return _defaultTimeoutMinutes; } + + internal int ResolveTunnelConnectTimeout() + { + var raw = Environment.GetEnvironmentVariable(_tunnelConnectTimeoutSeconds); + if (!string.IsNullOrEmpty(raw) && int.TryParse(raw, out var customTimeout) && customTimeout > 0) + { + Trace.Info($"Using custom tunnel connect timeout {customTimeout}s from {_tunnelConnectTimeoutSeconds}"); + return customTimeout; + } + + return _defaultTunnelConnectTimeoutSeconds; + } } } diff --git a/src/Runner.Worker/Dap/DebuggerConfig.cs b/src/Runner.Worker/Dap/DebuggerConfig.cs new file mode 100644 index 000000000..df139a15c --- /dev/null +++ b/src/Runner.Worker/Dap/DebuggerConfig.cs @@ -0,0 +1,33 @@ +using GitHub.DistributedTask.Pipelines; + +namespace GitHub.Runner.Worker.Dap +{ + /// + /// Consolidated runtime configuration for the job debugger. + /// Populated once from the acquire response and owned by . + /// + public sealed class DebuggerConfig + { + public DebuggerConfig(bool enabled, DebuggerTunnelInfo tunnel) + { + Enabled = enabled; + Tunnel = tunnel; + } + + /// Whether the debugger is enabled for this job. + public bool Enabled { get; } + + /// + /// Dev Tunnel details for remote debugging. + /// Required when is true. + /// + public DebuggerTunnelInfo Tunnel { get; } + + /// Whether the tunnel configuration is complete and valid. + public bool HasValidTunnel => Tunnel != null + && !string.IsNullOrEmpty(Tunnel.TunnelId) + && !string.IsNullOrEmpty(Tunnel.ClusterId) + && !string.IsNullOrEmpty(Tunnel.HostToken) + && Tunnel.Port >= 1024 && Tunnel.Port <= 65535; + } +} diff --git a/src/Runner.Worker/ExecutionContext.cs b/src/Runner.Worker/ExecutionContext.cs index 70f2a47af..6acb3e385 100644 --- a/src/Runner.Worker/ExecutionContext.cs +++ b/src/Runner.Worker/ExecutionContext.cs @@ -970,7 +970,7 @@ namespace GitHub.Runner.Worker Global.WriteDebug = Global.Variables.Step_Debug ?? false; // Debugger enabled flag (from acquire response). - Global.EnableDebugger = message.EnableDebugger; + Global.Debugger = new Dap.DebuggerConfig(message.EnableDebugger, message.DebuggerTunnel); // Hook up JobServerQueueThrottling event, we will log warning on server tarpit. _jobServerQueue.JobServerQueueThrottling += JobServerQueueThrottling_EventReceived; diff --git a/src/Runner.Worker/GlobalContext.cs b/src/Runner.Worker/GlobalContext.cs index 60b4ef1fe..b22b9f8ad 100644 --- a/src/Runner.Worker/GlobalContext.cs +++ b/src/Runner.Worker/GlobalContext.cs @@ -4,6 +4,7 @@ using GitHub.Actions.RunService.WebApi; using GitHub.DistributedTask.WebApi; using GitHub.Runner.Common.Util; using GitHub.Runner.Worker.Container; +using GitHub.Runner.Worker.Dap; using Newtonsoft.Json.Linq; using Sdk.RSWebApi.Contracts; @@ -27,7 +28,7 @@ namespace GitHub.Runner.Worker public StepsContext StepsContext { get; set; } public Variables Variables { get; set; } public bool WriteDebug { get; set; } - public bool EnableDebugger { get; set; } + public DebuggerConfig Debugger { get; set; } public string InfrastructureFailureCategory { get; set; } public JObject ContainerHookState { get; set; } public bool HasTemplateEvaluatorMismatch { get; set; } diff --git a/src/Runner.Worker/JobRunner.cs b/src/Runner.Worker/JobRunner.cs index 10623bbef..2ccad0c0c 100644 --- a/src/Runner.Worker/JobRunner.cs +++ b/src/Runner.Worker/JobRunner.cs @@ -182,7 +182,7 @@ namespace GitHub.Runner.Worker _tempDirectoryManager.InitializeTempDirectory(jobContext); // Setup the debugger - if (jobContext.Global.EnableDebugger) + if (jobContext.Global.Debugger?.Enabled == true) { Trace.Info("Debugger enabled for this job run"); diff --git a/src/Runner.Worker/Runner.Worker.csproj b/src/Runner.Worker/Runner.Worker.csproj index 4470920e1..ad8fbeb32 100644 --- a/src/Runner.Worker/Runner.Worker.csproj +++ b/src/Runner.Worker/Runner.Worker.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs b/src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs index 328f62160..465af8963 100644 --- a/src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs +++ b/src/Sdk/DTPipelines/Pipelines/AgentJobRequestMessage.cs @@ -260,6 +260,13 @@ namespace GitHub.DistributedTask.Pipelines set; } + [DataMember(EmitDefaultValue = false)] + public DebuggerTunnelInfo DebuggerTunnel + { + get; + set; + } + /// /// Gets the collection of variables associated with the current context. /// diff --git a/src/Sdk/DTPipelines/Pipelines/DebuggerTunnelInfo.cs b/src/Sdk/DTPipelines/Pipelines/DebuggerTunnelInfo.cs new file mode 100644 index 000000000..a47c10cb6 --- /dev/null +++ b/src/Sdk/DTPipelines/Pipelines/DebuggerTunnelInfo.cs @@ -0,0 +1,24 @@ +using System.Runtime.Serialization; + +namespace GitHub.DistributedTask.Pipelines +{ + /// + /// Dev Tunnel information the runner needs to host the debugger tunnel. + /// Matches the run-service DebuggerTunnel contract. + /// + [DataContract] + public sealed class DebuggerTunnelInfo + { + [DataMember(EmitDefaultValue = false)] + public string TunnelId { get; set; } + + [DataMember(EmitDefaultValue = false)] + public string ClusterId { get; set; } + + [DataMember(EmitDefaultValue = false)] + public string HostToken { get; set; } + + [DataMember(EmitDefaultValue = false)] + public ushort Port { get; set; } + } +} diff --git a/src/Test/L0/Sdk/RSWebApi/AgentJobRequestMessageL0.cs b/src/Test/L0/Sdk/RSWebApi/AgentJobRequestMessageL0.cs index 33b30d308..4756d3de0 100644 --- a/src/Test/L0/Sdk/RSWebApi/AgentJobRequestMessageL0.cs +++ b/src/Test/L0/Sdk/RSWebApi/AgentJobRequestMessageL0.cs @@ -69,6 +69,56 @@ public sealed class AgentJobRequestMessageL0 Assert.False(recoveredMessage.EnableDebugger, "EnableDebugger should be false when JSON contains 'EnableDebugger': false"); } + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void VerifyDebuggerTunnelDeserialization_WithTunnel() + { + // Arrange + var serializer = new DataContractJsonSerializer(typeof(AgentJobRequestMessage), new DataContractJsonSerializerSettings + { + KnownTypes = new[] { typeof(DebuggerTunnelInfo) } + }); + string json = DoubleQuotify( + "{'EnableDebugger': true, 'DebuggerTunnel': {'TunnelId': 'tun-123', 'ClusterId': 'use2', 'HostToken': 'tok-abc', 'Port': 4711}}"); + + // 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.True(recoveredMessage.EnableDebugger); + Assert.NotNull(recoveredMessage.DebuggerTunnel); + Assert.Equal("tun-123", recoveredMessage.DebuggerTunnel.TunnelId); + Assert.Equal("use2", recoveredMessage.DebuggerTunnel.ClusterId); + Assert.Equal("tok-abc", recoveredMessage.DebuggerTunnel.HostToken); + Assert.Equal(4711, recoveredMessage.DebuggerTunnel.Port); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Common")] + public void VerifyDebuggerTunnelDeserialization_WithoutTunnel() + { + // Arrange + var serializer = new DataContractJsonSerializer(typeof(AgentJobRequestMessage)); + string json = DoubleQuotify("{'EnableDebugger': true}"); + + // 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.True(recoveredMessage.EnableDebugger); + Assert.Null(recoveredMessage.DebuggerTunnel); + } + private static string DoubleQuotify(string text) { return text.Replace('\'', '"'); diff --git a/src/Test/L0/Worker/DapDebuggerL0.cs b/src/Test/L0/Worker/DapDebuggerL0.cs index f2c8557d1..3b7487dd9 100644 --- a/src/Test/L0/Worker/DapDebuggerL0.cs +++ b/src/Test/L0/Worker/DapDebuggerL0.cs @@ -16,8 +16,8 @@ namespace GitHub.Runner.Common.Tests.Worker { public sealed class DapDebuggerL0 { - private const string PortEnvironmentVariable = "ACTIONS_RUNNER_DAP_PORT"; private const string TimeoutEnvironmentVariable = "ACTIONS_RUNNER_DAP_CONNECTION_TIMEOUT"; + private const string TunnelConnectTimeoutVariable = "ACTIONS_RUNNER_DAP_TUNNEL_CONNECT_TIMEOUT_SECONDS"; private DapDebugger _debugger; private TestHostContext CreateTestContext([CallerMemberName] string testName = "") @@ -25,6 +25,7 @@ namespace GitHub.Runner.Common.Tests.Worker var hc = new TestHostContext(this, testName); _debugger = new DapDebugger(); _debugger.Initialize(hc); + _debugger.SkipTunnelRelay = true; return hc; } @@ -56,11 +57,11 @@ namespace GitHub.Runner.Common.Tests.Worker } } - private static int GetFreePort() + private static ushort GetFreePort() { using var listener = new TcpListener(IPAddress.Loopback, 0); listener.Start(); - return ((IPEndPoint)listener.LocalEndpoint).Port; + return (ushort)((IPEndPoint)listener.LocalEndpoint).Port; } private static async Task ConnectClientAsync(int port) @@ -140,10 +141,19 @@ namespace GitHub.Runner.Common.Tests.Worker return Encoding.UTF8.GetString(body); } - private static Mock CreateJobContext(CancellationToken cancellationToken, string jobName = null) + private static Mock CreateJobContextWithTunnel(CancellationToken cancellationToken, ushort port, string jobName = null) { + var tunnel = new GitHub.DistributedTask.Pipelines.DebuggerTunnelInfo + { + TunnelId = "test-tunnel", + ClusterId = "test-cluster", + HostToken = "test-token", + Port = port + }; + var debuggerConfig = new DebuggerConfig(true, tunnel); var jobContext = new Mock(); jobContext.Setup(x => x.CancellationToken).Returns(cancellationToken); + jobContext.Setup(x => x.Global).Returns(new GlobalContext { Debugger = debuggerConfig }); jobContext .Setup(x => x.GetGitHubContext(It.IsAny())) .Returns((string contextName) => string.Equals(contextName, "job", StringComparison.Ordinal) ? jobName : null); @@ -165,42 +175,36 @@ namespace GitHub.Runner.Common.Tests.Worker [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public void ResolvePortUsesCustomPortFromEnvironment() + public async Task StartAsyncFailsWithoutValidTunnelConfig() { using (CreateTestContext()) { - WithEnvironmentVariable(PortEnvironmentVariable, "9999", () => + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var jobContext = new Mock(); + jobContext.Setup(x => x.CancellationToken).Returns(cts.Token); + jobContext.Setup(x => x.Global).Returns(new GlobalContext { - Assert.Equal(9999, _debugger.ResolvePort()); + Debugger = new DebuggerConfig(true, null) }); + + await Assert.ThrowsAsync(() => _debugger.StartAsync(jobContext.Object)); } } [Fact] [Trait("Level", "L0")] [Trait("Category", "Worker")] - public void ResolvePortIgnoresInvalidPortFromEnvironment() + public async Task StartAsyncUsesPortFromTunnelConfig() { using (CreateTestContext()) { - WithEnvironmentVariable(PortEnvironmentVariable, "not-a-number", () => - { - Assert.Equal(4711, _debugger.ResolvePort()); - }); - } - } - - [Fact] - [Trait("Level", "L0")] - [Trait("Category", "Worker")] - public void ResolvePortIgnoresOutOfRangePortFromEnvironment() - { - using (CreateTestContext()) - { - WithEnvironmentVariable(PortEnvironmentVariable, "99999", () => - { - Assert.Equal(4711, _debugger.ResolvePort()); - }); + 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); + Assert.True(client.Connected); + await _debugger.StopAsync(); } } @@ -254,15 +258,12 @@ namespace GitHub.Runner.Common.Tests.Worker using (CreateTestContext()) { var port = GetFreePort(); - await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var jobContext = CreateJobContext(cts.Token); - await _debugger.StartAsync(jobContext.Object); - using var client = await ConnectClientAsync(port); - Assert.True(client.Connected); - await _debugger.StopAsync(); - }); + 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); + Assert.True(client.Connected); + await _debugger.StopAsync(); } } @@ -275,13 +276,10 @@ namespace GitHub.Runner.Common.Tests.Worker { foreach (var port in new[] { GetFreePort(), GetFreePort() }) { - await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var jobContext = CreateJobContext(cts.Token); - await _debugger.StartAsync(jobContext.Object); - await _debugger.StopAsync(); - }); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var jobContext = CreateJobContextWithTunnel(cts.Token, port); + await _debugger.StartAsync(jobContext.Object); + await _debugger.StopAsync(); } } } @@ -294,25 +292,22 @@ namespace GitHub.Runner.Common.Tests.Worker using (CreateTestContext()) { var port = GetFreePort(); - await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => + 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); + await SendRequestAsync(client.GetStream(), new Request { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var jobContext = CreateJobContext(cts.Token); - await _debugger.StartAsync(jobContext.Object); - - var waitTask = _debugger.WaitUntilReadyAsync(); - using var client = await ConnectClientAsync(port); - await SendRequestAsync(client.GetStream(), new Request - { - Seq = 1, - Type = "request", - Command = "configurationDone" - }); - - await waitTask; - Assert.Equal(DapSessionState.Ready, _debugger.State); - await _debugger.StopAsync(); + Seq = 1, + Type = "request", + Command = "configurationDone" }); + + await waitTask; + Assert.Equal(DapSessionState.Ready, _debugger.State); + await _debugger.StopAsync(); } } @@ -324,25 +319,22 @@ namespace GitHub.Runner.Common.Tests.Worker using (CreateTestContext()) { var port = GetFreePort(); - await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var jobContext = CreateJobContextWithTunnel(cts.Token, port, "ci-job"); + await _debugger.StartAsync(jobContext.Object); + using var client = await ConnectClientAsync(port); + var stream = client.GetStream(); + await SendRequestAsync(client.GetStream(), new Request { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var jobContext = CreateJobContext(cts.Token, "ci-job"); - await _debugger.StartAsync(jobContext.Object); - using var client = await ConnectClientAsync(port); - var stream = client.GetStream(); - await SendRequestAsync(client.GetStream(), new Request - { - Seq = 1, - Type = "request", - Command = "threads" - }); - - var response = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)); - Assert.Contains("\"command\":\"threads\"", response); - Assert.Contains("\"name\":\"Job: ci-job\"", response); - await _debugger.StopAsync(); + Seq = 1, + Type = "request", + Command = "threads" }); + + var response = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)); + Assert.Contains("\"command\":\"threads\"", response); + Assert.Contains("\"name\":\"Job: ci-job\"", response); + await _debugger.StopAsync(); } } @@ -354,30 +346,27 @@ namespace GitHub.Runner.Common.Tests.Worker using (CreateTestContext()) { var port = GetFreePort(); - await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => + 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); + await SendRequestAsync(client.GetStream(), new Request { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var jobContext = CreateJobContext(cts.Token); - await _debugger.StartAsync(jobContext.Object); - - var waitTask = _debugger.WaitUntilReadyAsync(); - using var client = await ConnectClientAsync(port); - await SendRequestAsync(client.GetStream(), new Request - { - Seq = 1, - Type = "request", - Command = "configurationDone" - }); - - await waitTask; - cts.Cancel(); - - // In the real runner, JobRunner always calls OnJobCompletedAsync - // from a finally block. The cancellation callback only unblocks - // pending waits; OnJobCompletedAsync handles state + cleanup. - await _debugger.OnJobCompletedAsync(); - Assert.Equal(DapSessionState.Terminated, _debugger.State); + Seq = 1, + Type = "request", + Command = "configurationDone" }); + + await waitTask; + cts.Cancel(); + + // In the real runner, JobRunner always calls OnJobCompletedAsync + // from a finally block. The cancellation callback only unblocks + // pending waits; OnJobCompletedAsync handles state + cleanup. + await _debugger.OnJobCompletedAsync(); + Assert.Equal(DapSessionState.Terminated, _debugger.State); } } @@ -400,25 +389,22 @@ namespace GitHub.Runner.Common.Tests.Worker using (CreateTestContext()) { var port = GetFreePort(); - await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => + 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); + await SendRequestAsync(client.GetStream(), new Request { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var jobContext = CreateJobContext(cts.Token); - await _debugger.StartAsync(jobContext.Object); - - var waitTask = _debugger.WaitUntilReadyAsync(); - using var client = await ConnectClientAsync(port); - await SendRequestAsync(client.GetStream(), new Request - { - Seq = 1, - Type = "request", - Command = "configurationDone" - }); - - await waitTask; - await _debugger.OnJobCompletedAsync(); - Assert.Equal(DapSessionState.Terminated, _debugger.State); + Seq = 1, + Type = "request", + Command = "configurationDone" }); + + await waitTask; + await _debugger.OnJobCompletedAsync(); + Assert.Equal(DapSessionState.Terminated, _debugger.State); } } @@ -441,20 +427,17 @@ namespace GitHub.Runner.Common.Tests.Worker using (CreateTestContext()) { var port = GetFreePort(); - await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var jobContext = CreateJobContext(cts.Token); - await _debugger.StartAsync(jobContext.Object); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var jobContext = CreateJobContextWithTunnel(cts.Token, port); + await _debugger.StartAsync(jobContext.Object); - var waitTask = _debugger.WaitUntilReadyAsync(); - await Task.Delay(50); - cts.Cancel(); + var waitTask = _debugger.WaitUntilReadyAsync(); + await Task.Delay(50); + cts.Cancel(); - var ex = await Assert.ThrowsAnyAsync(() => waitTask); - Assert.IsNotType(ex); - await _debugger.StopAsync(); - }); + var ex = await Assert.ThrowsAnyAsync(() => waitTask); + Assert.IsNotType(ex); + await _debugger.StopAsync(); } } @@ -471,32 +454,29 @@ namespace GitHub.Runner.Common.Tests.Worker hc.SecretMasker.AddValue("initialized"); var port = GetFreePort(); - await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => + 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 { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var jobContext = CreateJobContext(cts.Token); - 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 = "initialize" - }); - - var response = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)); - Assert.Contains("\"type\":\"response\"", response); - Assert.Contains("\"command\":\"initialize\"", response); - Assert.Contains("\"success\":true", response); - - var initializedEvent = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)); - Assert.Contains("\"type\":\"event\"", initializedEvent); - Assert.Contains("\"event\":\"initialized\"", initializedEvent); - - await _debugger.StopAsync(); + Seq = 1, + Type = "request", + Command = "initialize" }); + + var response = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)); + Assert.Contains("\"type\":\"response\"", response); + Assert.Contains("\"command\":\"initialize\"", response); + Assert.Contains("\"success\":true", response); + + var initializedEvent = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)); + Assert.Contains("\"type\":\"event\"", initializedEvent); + Assert.Contains("\"event\":\"initialized\"", initializedEvent); + + await _debugger.StopAsync(); } } @@ -508,41 +488,38 @@ namespace GitHub.Runner.Common.Tests.Worker using (CreateTestContext()) { var port = GetFreePort(); - await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var jobContext = CreateJobContextWithTunnel(cts.Token, port); + await _debugger.StartAsync(jobContext.Object); + + // Complete handshake so session is ready + var waitTask = _debugger.WaitUntilReadyAsync(); + using var client = await ConnectClientAsync(port); + var stream = client.GetStream(); + await SendRequestAsync(stream, new Request { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var jobContext = CreateJobContext(cts.Token); - await _debugger.StartAsync(jobContext.Object); - - // Complete handshake so session is ready - 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 waitTask; - - // Simulate a step starting (which pauses) - var step = new Mock(); - step.Setup(s => s.DisplayName).Returns("Test Step"); - step.Setup(s => s.ExecutionContext).Returns((IExecutionContext)null); - var stepTask = _debugger.OnStepStartingAsync(step.Object); - - // Give the step time to pause - await Task.Delay(50); - - // Cancel the job — should release the step pause - cts.Cancel(); - await stepTask; - - // In the real runner, OnJobCompletedAsync always follows. - await _debugger.OnJobCompletedAsync(); - Assert.Equal(DapSessionState.Terminated, _debugger.State); + Seq = 1, + Type = "request", + Command = "configurationDone" }); + await waitTask; + + // Simulate a step starting (which pauses) + var step = new Mock(); + step.Setup(s => s.DisplayName).Returns("Test Step"); + step.Setup(s => s.ExecutionContext).Returns((IExecutionContext)null); + var stepTask = _debugger.OnStepStartingAsync(step.Object); + + // Give the step time to pause + await Task.Delay(50); + + // Cancel the job — should release the step pause + cts.Cancel(); + await stepTask; + + // In the real runner, OnJobCompletedAsync always follows. + await _debugger.OnJobCompletedAsync(); + Assert.Equal(DapSessionState.Terminated, _debugger.State); } } @@ -558,13 +535,10 @@ namespace GitHub.Runner.Common.Tests.Worker // Start then immediate stop (no connection, no WaitUntilReady) var port = GetFreePort(); - await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var jobContext = CreateJobContext(cts.Token); - await _debugger.StartAsync(jobContext.Object); - await _debugger.StopAsync(); - }); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + var jobContext = CreateJobContextWithTunnel(cts.Token, port); + await _debugger.StartAsync(jobContext.Object); + await _debugger.StopAsync(); // StopAsync after already stopped await _debugger.StopAsync(); @@ -579,36 +553,86 @@ namespace GitHub.Runner.Common.Tests.Worker using (CreateTestContext()) { var port = GetFreePort(); - await WithEnvironmentVariableAsync(PortEnvironmentVariable, port.ToString(), async () => + 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 { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - var jobContext = CreateJobContext(cts.Token); - await _debugger.StartAsync(jobContext.Object); + Seq = 1, + Type = "request", + Command = "configurationDone" + }); - 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" - }); + // Read the configurationDone response + await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)); + await waitTask; - // Read the configurationDone response - await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)); - await waitTask; + // Complete the job — events are sent via OnJobCompletedAsync + await _debugger.OnJobCompletedAsync(); - // Complete the job — events are sent via OnJobCompletedAsync - await _debugger.OnJobCompletedAsync(); + var msg1 = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)); + var msg2 = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)); - var msg1 = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)); - var msg2 = await ReadDapMessageAsync(stream, TimeSpan.FromSeconds(5)); + // Both events should arrive (order may vary) + var combined = msg1 + msg2; + Assert.Contains("\"event\":\"terminated\"", combined); + Assert.Contains("\"event\":\"exited\"", combined); + } + } - // Both events should arrive (order may vary) - var combined = msg1 + msg2; - Assert.Contains("\"event\":\"terminated\"", combined); - Assert.Contains("\"event\":\"exited\"", combined); + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ResolveTunnelConnectTimeoutReturnsDefaultWhenNoVariable() + { + using (CreateTestContext()) + { + Assert.Equal(30, _debugger.ResolveTunnelConnectTimeout()); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ResolveTunnelConnectTimeoutUsesCustomValue() + { + using (CreateTestContext()) + { + WithEnvironmentVariable(TunnelConnectTimeoutVariable, "60", () => + { + Assert.Equal(60, _debugger.ResolveTunnelConnectTimeout()); + }); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ResolveTunnelConnectTimeoutIgnoresInvalidValue() + { + using (CreateTestContext()) + { + WithEnvironmentVariable(TunnelConnectTimeoutVariable, "not-a-number", () => + { + Assert.Equal(30, _debugger.ResolveTunnelConnectTimeout()); + }); + } + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Worker")] + public void ResolveTunnelConnectTimeoutIgnoresZeroValue() + { + using (CreateTestContext()) + { + WithEnvironmentVariable(TunnelConnectTimeoutVariable, "0", () => + { + Assert.Equal(30, _debugger.ResolveTunnelConnectTimeout()); }); } } From c9a1751d87b76a263d5a5d867f2033d69cb0d5da Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 08:40:32 +0100 Subject: [PATCH 10/15] Update Docker to v29.3.1 and Buildx to v0.33.0 (#4324) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Salman Chishti --- images/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/images/Dockerfile b/images/Dockerfile index c468b8d02..adfddf0b5 100644 --- a/images/Dockerfile +++ b/images/Dockerfile @@ -5,8 +5,8 @@ ARG TARGETOS ARG TARGETARCH ARG RUNNER_VERSION ARG RUNNER_CONTAINER_HOOKS_VERSION=0.7.0 -ARG DOCKER_VERSION=29.3.0 -ARG BUILDX_VERSION=0.32.1 +ARG DOCKER_VERSION=29.3.1 +ARG BUILDX_VERSION=0.33.0 RUN apt update -y && apt install curl unzip -y From 580116c18bc93672c530b91ee67f821e5f2308a6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 07:44:38 +0000 Subject: [PATCH 11/15] Bump @typescript-eslint/eslint-plugin from 8.57.2 to 8.58.1 in /src/Misc/expressionFunc/hashFiles (#4327) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Salman Chishti --- .../hashFiles/package-lock.json | 286 +++++++++--------- .../expressionFunc/hashFiles/package.json | 2 +- 2 files changed, 144 insertions(+), 144 deletions(-) diff --git a/src/Misc/expressionFunc/hashFiles/package-lock.json b/src/Misc/expressionFunc/hashFiles/package-lock.json index da1aa8383..d9207b2f3 100644 --- a/src/Misc/expressionFunc/hashFiles/package-lock.json +++ b/src/Misc/expressionFunc/hashFiles/package-lock.json @@ -14,7 +14,7 @@ "devDependencies": { "@stylistic/eslint-plugin": "^5.10.0", "@types/node": "^22.0.0", - "@typescript-eslint/eslint-plugin": "^8.57.2", + "@typescript-eslint/eslint-plugin": "^8.58.1", "@typescript-eslint/parser": "^8.0.0", "@vercel/ncc": "^0.38.3", "eslint": "^8.47.0", @@ -308,19 +308,19 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", - "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz", + "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.2", - "@typescript-eslint/type-utils": "8.57.2", - "@typescript-eslint/utils": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/type-utils": "8.58.1", + "@typescript-eslint/utils": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -330,9 +330,9 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.57.2", + "@typescript-eslint/parser": "^8.58.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { @@ -346,9 +346,9 @@ } }, "node_modules/@typescript-eslint/eslint-plugin/node_modules/ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "engines": { "node": ">=18.12" @@ -358,15 +358,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", - "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz", + "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", "debug": "^4.4.3" }, "engines": { @@ -378,17 +378,17 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", - "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz", + "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==", "dev": true, "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.2", - "@typescript-eslint/types": "^8.57.2", + "@typescript-eslint/tsconfig-utils": "^8.58.1", + "@typescript-eslint/types": "^8.58.1", "debug": "^4.4.3" }, "engines": { @@ -399,17 +399,17 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", - "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz", + "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2" + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -420,9 +420,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", - "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz", + "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -432,20 +432,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", - "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz", + "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2", - "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/utils": "8.58.1", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -456,7 +456,7 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/type-utils/node_modules/ts-api-utils": { @@ -472,9 +472,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", - "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", + "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -485,20 +485,20 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", - "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz", + "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==", "dev": true, "dependencies": { - "@typescript-eslint/project-service": "8.57.2", - "@typescript-eslint/tsconfig-utils": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2", + "@typescript-eslint/project-service": "8.58.1", + "@typescript-eslint/tsconfig-utils": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -508,7 +508,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { @@ -533,12 +533,12 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" @@ -560,15 +560,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", - "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz", + "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2" + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -579,16 +579,16 @@ }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", - "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz", + "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/types": "8.58.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -4289,13 +4289,13 @@ "dev": true }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -4922,19 +4922,19 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", - "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.1.tgz", + "integrity": "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.2", - "@typescript-eslint/type-utils": "8.57.2", - "@typescript-eslint/utils": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/type-utils": "8.58.1", + "@typescript-eslint/utils": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "dependencies": { "ignore": { @@ -4944,66 +4944,66 @@ "dev": true }, "ts-api-utils": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", - "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "requires": {} } } }, "@typescript-eslint/parser": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", - "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.1.tgz", + "integrity": "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2", + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", "debug": "^4.4.3" } }, "@typescript-eslint/project-service": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", - "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.1.tgz", + "integrity": "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g==", "dev": true, "requires": { - "@typescript-eslint/tsconfig-utils": "^8.57.2", - "@typescript-eslint/types": "^8.57.2", + "@typescript-eslint/tsconfig-utils": "^8.58.1", + "@typescript-eslint/types": "^8.58.1", "debug": "^4.4.3" } }, "@typescript-eslint/scope-manager": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", - "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.1.tgz", + "integrity": "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w==", "dev": true, "requires": { - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2" + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1" } }, "@typescript-eslint/tsconfig-utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", - "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz", + "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==", "dev": true, "requires": {} }, "@typescript-eslint/type-utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", - "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.1.tgz", + "integrity": "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w==", "dev": true, "requires": { - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2", - "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1", + "@typescript-eslint/utils": "8.58.1", "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "dependencies": { "ts-api-utils": { @@ -5016,26 +5016,26 @@ } }, "@typescript-eslint/types": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", - "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.1.tgz", + "integrity": "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", - "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.1.tgz", + "integrity": "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg==", "dev": true, "requires": { - "@typescript-eslint/project-service": "8.57.2", - "@typescript-eslint/tsconfig-utils": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/visitor-keys": "8.57.2", + "@typescript-eslint/project-service": "8.58.1", + "@typescript-eslint/tsconfig-utils": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/visitor-keys": "8.58.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.5.0" }, "dependencies": { "balanced-match": { @@ -5054,12 +5054,12 @@ } }, "minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "requires": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" } }, "ts-api-utils": { @@ -5072,24 +5072,24 @@ } }, "@typescript-eslint/utils": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", - "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.1.tgz", + "integrity": "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.2", - "@typescript-eslint/types": "8.57.2", - "@typescript-eslint/typescript-estree": "8.57.2" + "@typescript-eslint/scope-manager": "8.58.1", + "@typescript-eslint/types": "8.58.1", + "@typescript-eslint/typescript-estree": "8.58.1" } }, "@typescript-eslint/visitor-keys": { - "version": "8.57.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", - "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", + "version": "8.58.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.1.tgz", + "integrity": "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ==", "dev": true, "requires": { - "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/types": "8.58.1", "eslint-visitor-keys": "^5.0.0" }, "dependencies": { @@ -7594,13 +7594,13 @@ "dev": true }, "tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, "requires": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "dependencies": { "fdir": { diff --git a/src/Misc/expressionFunc/hashFiles/package.json b/src/Misc/expressionFunc/hashFiles/package.json index 0097a84b7..a3adf0b9e 100644 --- a/src/Misc/expressionFunc/hashFiles/package.json +++ b/src/Misc/expressionFunc/hashFiles/package.json @@ -37,7 +37,7 @@ "devDependencies": { "@stylistic/eslint-plugin": "^5.10.0", "@types/node": "^22.0.0", - "@typescript-eslint/eslint-plugin": "^8.57.2", + "@typescript-eslint/eslint-plugin": "^8.58.1", "@typescript-eslint/parser": "^8.0.0", "@vercel/ncc": "^0.38.3", "eslint": "^8.47.0", From 1ba5fdfd8898283cda9d1b4df499551f59ab0832 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 08:54:56 +0100 Subject: [PATCH 12/15] Bump actions/github-script from 8 to 9 (#4331) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/docker-publish.yml | 2 +- .github/workflows/release.yml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5b4a6e758..f867297e3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -99,7 +99,7 @@ jobs: - name: Get latest runner version id: latest_runner - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: github-token: ${{secrets.GITHUB_TOKEN}} script: | diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 4704abeae..6f72d3e75 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -26,7 +26,7 @@ jobs: - name: Compute image version id: image - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | const fs = require('fs'); diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f46052449..2c817ce53 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: # Make sure ./releaseVersion match ./src/runnerversion # Query GitHub release ensure version is not used - name: Check version - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: github-token: ${{secrets.GITHUB_TOKEN}} script: | @@ -171,7 +171,7 @@ jobs: # Create ReleaseNote file - name: Create ReleaseNote id: releaseNote - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: github-token: ${{secrets.GITHUB_TOKEN}} script: | @@ -300,7 +300,7 @@ jobs: - name: Compute image version id: image - uses: actions/github-script@v8 + uses: actions/github-script@v9 with: script: | const fs = require('fs'); From 2bcc65e8649a3c6cb7017b563945ea19cf05dc6c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Apr 2026 09:01:34 +0100 Subject: [PATCH 13/15] Bump typescript from 5.9.3 to 6.0.2 in /src/Misc/expressionFunc/hashFiles (#4329) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Salman Chishti --- .../expressionFunc/hashFiles/package-lock.json | 14 +++++++------- src/Misc/expressionFunc/hashFiles/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Misc/expressionFunc/hashFiles/package-lock.json b/src/Misc/expressionFunc/hashFiles/package-lock.json index d9207b2f3..8c057ffbd 100644 --- a/src/Misc/expressionFunc/hashFiles/package-lock.json +++ b/src/Misc/expressionFunc/hashFiles/package-lock.json @@ -23,7 +23,7 @@ "husky": "^9.1.7", "lint-staged": "^15.5.0", "prettier": "^3.0.3", - "typescript": "^5.9.3" + "typescript": "^6.0.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -4486,9 +4486,9 @@ } }, "node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -7726,9 +7726,9 @@ } }, "typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.2.tgz", + "integrity": "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ==", "dev": true }, "unbox-primitive": { diff --git a/src/Misc/expressionFunc/hashFiles/package.json b/src/Misc/expressionFunc/hashFiles/package.json index a3adf0b9e..de0b71830 100644 --- a/src/Misc/expressionFunc/hashFiles/package.json +++ b/src/Misc/expressionFunc/hashFiles/package.json @@ -46,6 +46,6 @@ "husky": "^9.1.7", "lint-staged": "^15.5.0", "prettier": "^3.0.3", - "typescript": "^5.9.3" + "typescript": "^6.0.2" } } From 8d35e710da62c73fabf42c20da1485f54e078894 Mon Sep 17 00:00:00 2001 From: Salman Chishti Date: Fri, 10 Apr 2026 11:34:08 +0100 Subject: [PATCH 14/15] fix: only show changed versions in node upgrade PR description (#4332) --- .github/workflows/node-upgrade.yml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/node-upgrade.yml b/.github/workflows/node-upgrade.yml index 3d6122f42..bec03a1b1 100644 --- a/.github/workflows/node-upgrade.yml +++ b/.github/workflows/node-upgrade.yml @@ -159,18 +159,36 @@ jobs: git config --global user.name "github-actions[bot]" git config --global user.email "<41898282+github-actions[bot]@users.noreply.github.com>" + # Build version summary for commit message and PR body (only include changed versions) + COMMIT_VERSIONS="" + PR_VERSION_LINES="" + + if [ "${{ steps.node-versions.outputs.needs_update20 }}" == "true" ]; then + COMMIT_VERSIONS="20: $NODE20_VERSION" + PR_VERSION_LINES="- Node 20: ${{ steps.node-versions.outputs.current_node20 }} → $NODE20_VERSION" + fi + + if [ "${{ steps.node-versions.outputs.needs_update24 }}" == "true" ]; then + if [ -n "$COMMIT_VERSIONS" ]; then + COMMIT_VERSIONS="$COMMIT_VERSIONS, 24: $NODE24_VERSION" + else + COMMIT_VERSIONS="24: $NODE24_VERSION" + fi + PR_VERSION_LINES="${PR_VERSION_LINES:+$PR_VERSION_LINES + }- Node 24: ${{ steps.node-versions.outputs.current_node24 }} → $NODE24_VERSION" + fi + # Create branch and commit changes branch_name="chore/update-node" git checkout -b "$branch_name" - git commit -a -m "chore: update Node versions (20: $NODE20_VERSION, 24: $NODE24_VERSION)" + git commit -a -m "chore: update Node versions ($COMMIT_VERSIONS)" git push --force origin "$branch_name" # Create PR body using here-doc for proper formatting cat > pr_body.txt << EOF Automated Node.js version update: - - Node 20: ${{ steps.node-versions.outputs.current_node20 }} → $NODE20_VERSION - - Node 24: ${{ steps.node-versions.outputs.current_node24 }} → $NODE24_VERSION + $PR_VERSION_LINES This update ensures we're using the latest stable Node.js versions for security and performance improvements. From 182a4337823fd2c1c44fa8797b02e54b3ca8aebe Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 12:40:28 +0100 Subject: [PATCH 15/15] Bump System.Formats.Asn1, Cryptography.Pkcs, ProtectedData, ServiceController, CodePages, Threading.Channels, @actions/glob, @typescript-eslint/parser, lint-staged, picomatch (#4333) Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Salman Chishti --- .../hashFiles/package-lock.json | 797 ++++++++---------- .../expressionFunc/hashFiles/package.json | 6 +- src/Runner.Common/Runner.Common.csproj | 6 +- src/Runner.Listener/Runner.Listener.csproj | 4 +- src/Runner.Sdk/Runner.Sdk.csproj | 4 +- src/Runner.Worker/Runner.Worker.csproj | 6 +- src/Sdk/Sdk.csproj | 6 +- 7 files changed, 372 insertions(+), 457 deletions(-) diff --git a/src/Misc/expressionFunc/hashFiles/package-lock.json b/src/Misc/expressionFunc/hashFiles/package-lock.json index 8c057ffbd..1b4e0f37c 100644 --- a/src/Misc/expressionFunc/hashFiles/package-lock.json +++ b/src/Misc/expressionFunc/hashFiles/package-lock.json @@ -9,19 +9,19 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@actions/glob": "^0.4.0" + "@actions/glob": "^0.6.1" }, "devDependencies": { "@stylistic/eslint-plugin": "^5.10.0", "@types/node": "^22.0.0", "@typescript-eslint/eslint-plugin": "^8.58.1", - "@typescript-eslint/parser": "^8.0.0", + "@typescript-eslint/parser": "^8.58.1", "@vercel/ncc": "^0.38.3", "eslint": "^8.47.0", "eslint-plugin-github": "^4.10.2", "eslint-plugin-prettier": "^5.0.0", "husky": "^9.1.7", - "lint-staged": "^15.5.0", + "lint-staged": "^16.4.0", "prettier": "^3.0.3", "typescript": "^6.0.2" } @@ -36,31 +36,50 @@ } }, "node_modules/@actions/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz", - "integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz", + "integrity": "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==", + "license": "MIT", "dependencies": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" + "@actions/exec": "^3.0.0", + "@actions/http-client": "^4.0.0" + } + }, + "node_modules/@actions/exec": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz", + "integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==", + "license": "MIT", + "dependencies": { + "@actions/io": "^3.0.2" } }, "node_modules/@actions/glob": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.4.0.tgz", - "integrity": "sha512-+eKIGFhsFa4EBwaf/GMyzCdWrXWymGXfFmZU3FHQvYS8mPcHtTtZONbkcqqUMzw9mJ/pImEBFET1JNifhqGsAQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.6.1.tgz", + "integrity": "sha512-K4+2Ac5ILcf2ySdJCha+Pop9NcKjxqCL4xL4zI50dgB2PbXgC0+AcP011xfH4Of6b4QEJJg8dyZYv7zl4byTsw==", + "license": "MIT", "dependencies": { - "@actions/core": "^1.9.1", + "@actions/core": "^3.0.0", "minimatch": "^3.0.4" } }, "node_modules/@actions/http-client": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.1.1.tgz", - "integrity": "sha512-qhrkRMB40bbbLo7gF+0vu+X+UawOvQQqNAA/5Unx774RS8poaOhThDOG6BGmxvAnxhQnDp2BG/ZUm65xZILTpw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz", + "integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==", + "license": "MIT", "dependencies": { - "tunnel": "^0.0.6" + "tunnel": "^0.0.6", + "undici": "^6.23.0" } }, + "node_modules/@actions/io": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz", + "integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==", + "license": "MIT" + }, "node_modules/@babel/runtime": { "version": "7.26.10", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", @@ -279,9 +298,9 @@ } }, "node_modules/@stylistic/eslint-plugin/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -650,10 +669,11 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -666,9 +686,9 @@ } }, "node_modules/ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", "dev": true, "license": "MIT", "dependencies": { @@ -1042,17 +1062,17 @@ } }, "node_modules/cli-truncate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", "dev": true, "license": "MIT", "dependencies": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1084,13 +1104,13 @@ "license": "MIT" }, "node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/concat-map": { @@ -2040,9 +2060,9 @@ } }, "node_modules/eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "dev": true, "license": "MIT" }, @@ -2240,9 +2260,9 @@ } }, "node_modules/get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", "dev": true, "license": "MIT", "engines": { @@ -2671,13 +2691,16 @@ } }, "node_modules/is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", "dev": true, "license": "MIT", + "dependencies": { + "get-east-asian-width": "^1.3.1" + }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2975,128 +2998,51 @@ "node": ">= 0.8.0" } }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, "node_modules/lint-staged": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.0.tgz", - "integrity": "sha512-WyCzSbfYGhK7cU+UuDDkzUiytbfbi0ZdPy2orwtM75P3WTtQBzmG40cCxIa8Ii2+XjfxzLH6Be46tUfWS85Xfg==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", + "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^5.4.1", - "commander": "^13.1.0", - "debug": "^4.4.0", - "execa": "^8.0.1", - "lilconfig": "^3.1.3", - "listr2": "^8.2.5", - "micromatch": "^4.0.8", - "pidtree": "^0.6.0", + "commander": "^14.0.3", + "listr2": "^9.0.5", + "picomatch": "^4.0.3", "string-argv": "^0.3.2", - "yaml": "^2.7.0" + "tinyexec": "^1.0.4", + "yaml": "^2.8.2" }, "bin": { "lint-staged": "bin/lint-staged.js" }, "engines": { - "node": ">=18.12.0" + "node": ">=20.17" }, "funding": { "url": "https://opencollective.com/lint-staged" } }, - "node_modules/lint-staged/node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "node_modules/lint-staged/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" + "node": ">=12" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/lint-staged/node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/lint-staged/node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lint-staged/node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=16.17.0" - } - }, - "node_modules/lint-staged/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/listr2": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", - "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, "license": "MIT", "dependencies": { - "cli-truncate": "^4.0.0", + "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", @@ -3104,7 +3050,7 @@ "wrap-ansi": "^9.0.0" }, "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" } }, "node_modules/locate-path": { @@ -3173,9 +3119,9 @@ } }, "node_modules/log-update/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -3186,9 +3132,9 @@ } }, "node_modules/log-update/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { @@ -3198,26 +3144,10 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/log-update/node_modules/is-fullwidth-code-point": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", - "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/log-update/node_modules/slice-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", - "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "dev": true, "license": "MIT", "dependencies": { @@ -3232,13 +3162,13 @@ } }, "node_modules/log-update/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -3613,10 +3543,11 @@ "dev": true }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -3624,18 +3555,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", - "dev": true, - "bin": { - "pidtree": "bin/pidtree.js" - }, - "engines": { - "node": ">=0.10" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4050,26 +3969,26 @@ } }, "node_modules/slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" }, "engines": { - "node": ">=12" + "node": ">=20" }, "funding": { "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, "node_modules/slice-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { @@ -4089,27 +4008,26 @@ } }, "node_modules/string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -4119,21 +4037,14 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/string-width/node_modules/emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true, - "license": "MIT" - }, "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -4288,6 +4199,16 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "node_modules/tinyexec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -4392,6 +4313,7 @@ "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "license": "MIT", "engines": { "node": ">=0.6.11 <=0.7.0 || >=0.7.3" } @@ -4513,6 +4435,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -4568,14 +4499,6 @@ "punycode": "^2.1.0" } }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4627,9 +4550,9 @@ } }, "node_modules/wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "license": "MIT", "dependencies": { @@ -4645,9 +4568,9 @@ } }, "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -4658,9 +4581,9 @@ } }, "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { @@ -4670,14 +4593,39 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -4693,16 +4641,19 @@ "dev": true }, "node_modules/yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yocto-queue": { @@ -4726,31 +4677,45 @@ "dev": true }, "@actions/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@actions/core/-/core-1.10.0.tgz", - "integrity": "sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/core/-/core-3.0.0.tgz", + "integrity": "sha512-zYt6cz+ivnTmiT/ksRVriMBOiuoUpDCJJlZ5KPl2/FRdvwU3f7MPh9qftvbkXJThragzUZieit2nyHUyw53Seg==", "requires": { - "@actions/http-client": "^2.0.1", - "uuid": "^8.3.2" + "@actions/exec": "^3.0.0", + "@actions/http-client": "^4.0.0" + } + }, + "@actions/exec": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@actions/exec/-/exec-3.0.0.tgz", + "integrity": "sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==", + "requires": { + "@actions/io": "^3.0.2" } }, "@actions/glob": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.4.0.tgz", - "integrity": "sha512-+eKIGFhsFa4EBwaf/GMyzCdWrXWymGXfFmZU3FHQvYS8mPcHtTtZONbkcqqUMzw9mJ/pImEBFET1JNifhqGsAQ==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@actions/glob/-/glob-0.6.1.tgz", + "integrity": "sha512-K4+2Ac5ILcf2ySdJCha+Pop9NcKjxqCL4xL4zI50dgB2PbXgC0+AcP011xfH4Of6b4QEJJg8dyZYv7zl4byTsw==", "requires": { - "@actions/core": "^1.9.1", + "@actions/core": "^3.0.0", "minimatch": "^3.0.4" } }, "@actions/http-client": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-2.1.1.tgz", - "integrity": "sha512-qhrkRMB40bbbLo7gF+0vu+X+UawOvQQqNAA/5Unx774RS8poaOhThDOG6BGmxvAnxhQnDp2BG/ZUm65xZILTpw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@actions/http-client/-/http-client-4.0.0.tgz", + "integrity": "sha512-QuwPsgVMsD6qaPD57GLZi9sqzAZCtiJT8kVBCDpLtxhL5MydQ4gS+DrejtZZPdIYyB1e95uCK9Luyds7ybHI3g==", "requires": { - "tunnel": "^0.0.6" + "tunnel": "^0.0.6", + "undici": "^6.23.0" } }, + "@actions/io": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@actions/io/-/io-3.0.2.tgz", + "integrity": "sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==" + }, "@babel/runtime": { "version": "7.26.10", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz", @@ -4899,9 +4864,9 @@ } }, "picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true } } @@ -4947,8 +4912,7 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "requires": {} + "dev": true } } }, @@ -4990,8 +4954,7 @@ "version": "8.58.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.1.tgz", "integrity": "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw==", - "dev": true, - "requires": {} + "dev": true }, "@typescript-eslint/type-utils": { "version": "8.58.1", @@ -5010,8 +4973,7 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "requires": {} + "dev": true } } }, @@ -5066,8 +5028,7 @@ "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "requires": {} + "dev": true } } }, @@ -5123,13 +5084,12 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} + "dev": true }, "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "requires": { "fast-deep-equal": "^3.1.1", @@ -5139,9 +5099,9 @@ } }, "ansi-escapes": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", - "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", "dev": true, "requires": { "environment": "^1.0.0" @@ -5385,13 +5345,13 @@ } }, "cli-truncate": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", - "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.2.0.tgz", + "integrity": "sha512-xRwvIOMGrfOAnM1JYtqQImuaNtDEv9v6oIYAs4LIHwTiKee8uwvIi363igssOC0O5U04i4AlENs79LQLu9tEMw==", "dev": true, "requires": { - "slice-ansi": "^5.0.0", - "string-width": "^7.0.0" + "slice-ansi": "^8.0.0", + "string-width": "^8.2.0" } }, "color-convert": { @@ -5416,9 +5376,9 @@ "dev": true }, "commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz", + "integrity": "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw==", "dev": true }, "concat-map": { @@ -5678,8 +5638,7 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", - "dev": true, - "requires": {} + "dev": true }, "eslint-import-resolver-node": { "version": "0.3.9", @@ -5913,8 +5872,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-i18n-text/-/eslint-plugin-i18n-text-1.0.1.tgz", "integrity": "sha512-3G3UetST6rdqhqW9SfcfzNYMpQXS7wNkJvp6dsXnjzGiku6Iu5hl3B0kmk6lIcFPwYjhQIY+tXVRtK9TlGT7RA==", - "dev": true, - "requires": {} + "dev": true }, "eslint-plugin-import": { "version": "2.28.1", @@ -6079,9 +6037,9 @@ "dev": true }, "eventemitter3": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "dev": true }, "execa": { @@ -6242,9 +6200,9 @@ "dev": true }, "get-east-asian-width": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", - "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", "dev": true }, "get-intrinsic": { @@ -6524,10 +6482,13 @@ "dev": true }, "is-fullwidth-code-point": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", - "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", - "dev": true + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.1.0.tgz", + "integrity": "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==", + "dev": true, + "requires": { + "get-east-asian-width": "^1.3.1" + } }, "is-glob": { "version": "4.0.3", @@ -6731,80 +6692,35 @@ "type-check": "~0.4.0" } }, - "lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "dev": true - }, "lint-staged": { - "version": "15.5.0", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.5.0.tgz", - "integrity": "sha512-WyCzSbfYGhK7cU+UuDDkzUiytbfbi0ZdPy2orwtM75P3WTtQBzmG40cCxIa8Ii2+XjfxzLH6Be46tUfWS85Xfg==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-16.4.0.tgz", + "integrity": "sha512-lBWt8hujh/Cjysw5GYVmZpFHXDCgZzhrOm8vbcUdobADZNOK/bRshr2kM3DfgrrtR1DQhfupW9gnIXOfiFi+bw==", "dev": true, "requires": { - "chalk": "^5.4.1", - "commander": "^13.1.0", - "debug": "^4.4.0", - "execa": "^8.0.1", - "lilconfig": "^3.1.3", - "listr2": "^8.2.5", - "micromatch": "^4.0.8", - "pidtree": "^0.6.0", + "commander": "^14.0.3", + "listr2": "^9.0.5", + "picomatch": "^4.0.3", "string-argv": "^0.3.2", - "yaml": "^2.7.0" + "tinyexec": "^1.0.4", + "yaml": "^2.8.2" }, "dependencies": { - "chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "dev": true - }, - "execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - } - }, - "get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true - }, - "human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true - }, - "signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true } } }, "listr2": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", - "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-9.0.5.tgz", + "integrity": "sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==", "dev": true, "requires": { - "cli-truncate": "^4.0.0", + "cli-truncate": "^5.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", @@ -6865,30 +6781,21 @@ }, "dependencies": { "ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true }, "ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true }, - "is-fullwidth-code-point": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", - "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", - "dev": true, - "requires": { - "get-east-asian-width": "^1.0.0" - } - }, "slice-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", - "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.2.tgz", + "integrity": "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==", "dev": true, "requires": { "ansi-styles": "^6.2.1", @@ -6896,12 +6803,12 @@ } }, "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "requires": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" } } } @@ -7166,15 +7073,9 @@ "dev": true }, "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pidtree": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", - "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true }, "prelude-ls": { @@ -7439,19 +7340,19 @@ "dev": true }, "slice-ansi": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", - "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-8.0.0.tgz", + "integrity": "sha512-stxByr12oeeOyY2BlviTNQlYV5xOj47GirPr4yA1hE9JCtxfQN0+tVbkxwCtYDQWhEKWFHsEK48ORg5jrouCAg==", "dev": true, "requires": { - "ansi-styles": "^6.0.0", - "is-fullwidth-code-point": "^4.0.0" + "ansi-styles": "^6.2.3", + "is-fullwidth-code-point": "^5.1.0" }, "dependencies": { "ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true } } @@ -7463,35 +7364,28 @@ "dev": true }, "string-width": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", - "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.0.tgz", + "integrity": "sha512-6hJPQ8N0V0P3SNmP6h2J99RLuzrWz2gvT7VnK5tKvrNqJoyS9W4/Fb8mo31UiPvy00z7DQXkP2hnKBVav76thw==", "dev": true, "requires": { - "emoji-regex": "^10.3.0", - "get-east-asian-width": "^1.0.0", - "strip-ansi": "^7.1.0" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, "dependencies": { "ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true - }, - "emoji-regex": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", - "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true }, "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "requires": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" } } } @@ -7593,6 +7487,12 @@ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", "dev": true }, + "tinyexec": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.1.tgz", + "integrity": "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==", + "dev": true + }, "tinyglobby": { "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", @@ -7607,8 +7507,7 @@ "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "requires": {} + "dev": true }, "picomatch": { "version": "4.0.4", @@ -7637,8 +7536,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", - "dev": true, - "requires": {} + "dev": true }, "tsconfig-paths": { "version": "3.14.2", @@ -7743,6 +7641,11 @@ "which-boxed-primitive": "^1.0.2" } }, + "undici": { + "version": "6.24.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz", + "integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==" + }, "undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -7774,11 +7677,6 @@ "punycode": "^2.1.0" } }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -7815,9 +7713,9 @@ } }, "wrap-ansi": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", - "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", "dev": true, "requires": { "ansi-styles": "^6.2.1", @@ -7826,24 +7724,41 @@ }, "dependencies": { "ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true }, "ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true }, - "strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true + }, + "string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, "requires": { - "ansi-regex": "^6.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + } + }, + "strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "requires": { + "ansi-regex": "^6.2.2" } } } @@ -7855,9 +7770,9 @@ "dev": true }, "yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true }, "yocto-queue": { diff --git a/src/Misc/expressionFunc/hashFiles/package.json b/src/Misc/expressionFunc/hashFiles/package.json index de0b71830..19b00530c 100644 --- a/src/Misc/expressionFunc/hashFiles/package.json +++ b/src/Misc/expressionFunc/hashFiles/package.json @@ -32,19 +32,19 @@ "author": "GitHub Actions", "license": "MIT", "dependencies": { - "@actions/glob": "^0.4.0" + "@actions/glob": "^0.6.1" }, "devDependencies": { "@stylistic/eslint-plugin": "^5.10.0", "@types/node": "^22.0.0", "@typescript-eslint/eslint-plugin": "^8.58.1", - "@typescript-eslint/parser": "^8.0.0", + "@typescript-eslint/parser": "^8.58.1", "@vercel/ncc": "^0.38.3", "eslint": "^8.47.0", "eslint-plugin-github": "^4.10.2", "eslint-plugin-prettier": "^5.0.0", "husky": "^9.1.7", - "lint-staged": "^15.5.0", + "lint-staged": "^16.4.0", "prettier": "^3.0.3", "typescript": "^6.0.2" } diff --git a/src/Runner.Common/Runner.Common.csproj b/src/Runner.Common/Runner.Common.csproj index 6c4635626..96b417c40 100644 --- a/src/Runner.Common/Runner.Common.csproj +++ b/src/Runner.Common/Runner.Common.csproj @@ -17,9 +17,9 @@ - - - + + + diff --git a/src/Runner.Listener/Runner.Listener.csproj b/src/Runner.Listener/Runner.Listener.csproj index 68df8fbbc..e90e6b2b0 100644 --- a/src/Runner.Listener/Runner.Listener.csproj +++ b/src/Runner.Listener/Runner.Listener.csproj @@ -22,8 +22,8 @@ - - + + diff --git a/src/Runner.Sdk/Runner.Sdk.csproj b/src/Runner.Sdk/Runner.Sdk.csproj index 55dbf1262..6d6cf6e4b 100644 --- a/src/Runner.Sdk/Runner.Sdk.csproj +++ b/src/Runner.Sdk/Runner.Sdk.csproj @@ -15,9 +15,9 @@ - + - + diff --git a/src/Runner.Worker/Runner.Worker.csproj b/src/Runner.Worker/Runner.Worker.csproj index ad8fbeb32..d1c130d74 100644 --- a/src/Runner.Worker/Runner.Worker.csproj +++ b/src/Runner.Worker/Runner.Worker.csproj @@ -19,9 +19,9 @@ - - - + + + diff --git a/src/Sdk/Sdk.csproj b/src/Sdk/Sdk.csproj index 644bf887e..9b0bccc97 100644 --- a/src/Sdk/Sdk.csproj +++ b/src/Sdk/Sdk.csproj @@ -23,14 +23,14 @@ - - + + - +