9.1 KiB
DAP Debugging - Bug Fixes and Enhancements
Status: Planned
Date: January 2026
Related: dap-debugging.md
Overview
This document tracks bug fixes and enhancements for the DAP debugging implementation after the initial phases were completed.
Issues
Bug 1: Double Output in REPL Shell Commands
Symptom: Running commands in the REPL shell produces double output - the first one unmasked, the second one with secrets masked.
Root Cause: In DapDebugSession.ExecuteShellCommandAsync() (lines 670-773), output is sent to the debugger twice:
- Real-time streaming (unmasked): Lines 678-712 stream output via DAP
outputevents as data arrives from the process - but this output is NOT masked - Final result (masked): Lines 765-769 return the combined output as
EvaluateResponseBody.Resultwith secrets masked
The DAP client displays both the streamed events AND the evaluate response result, causing duplication.
Fix:
- Mask secrets in the real-time streaming output (add
HostContext.SecretMasker.MaskSecrets()to lines ~690 and ~708) - Change the final
Resultto only show exit code summary instead of full output
Bug 2: Expressions Interpreted as Shell Commands
Symptom: Evaluating expressions like ${{github.event_name}} == 'push' in the Watch/Expressions pane results in them being executed as shell commands instead of being evaluated as GitHub Actions expressions.
Root Cause: In DapDebugSession.HandleEvaluateAsync() (line 514), the condition to detect shell commands is too broad:
if (evalContext == "repl" || expression.StartsWith("!") || expression.StartsWith("$"))
Since ${{github.event_name}} starts with $, it gets routed to shell execution instead of expression evaluation.
Fix:
- Check for
${{prefix first - these are always GitHub Actions expressions - Remove the
expression.StartsWith("$")condition entirely (ambiguous and unnecessary since REPL context handles shell commands) - Keep
expression.StartsWith("!")for explicit shell override in non-REPL contexts
Enhancement: Expression Interpolation in REPL Commands
Request: When running REPL commands like echo ${{github.event_name}}, the ${{ }} expressions should be expanded before shell execution, similar to how run: steps work.
Approach: Add a helper method that uses the existing PipelineTemplateEvaluator infrastructure to expand expressions in the command string before passing it to the shell.
Implementation Details
File: src/Runner.Worker/Dap/DapDebugSession.cs
Change 1: Mask Real-Time Streaming Output
Location: Lines ~678-712 (OutputDataReceived and ErrorDataReceived handlers)
Before:
processInvoker.OutputDataReceived += (sender, args) =>
{
if (!string.IsNullOrEmpty(args.Data))
{
output.AppendLine(args.Data);
_server?.SendEvent(new Event
{
EventType = "output",
Body = new OutputEventBody
{
Category = "stdout",
Output = args.Data + "\n" // NOT MASKED
}
});
}
};
After:
processInvoker.OutputDataReceived += (sender, args) =>
{
if (!string.IsNullOrEmpty(args.Data))
{
output.AppendLine(args.Data);
var maskedData = HostContext.SecretMasker.MaskSecrets(args.Data);
_server?.SendEvent(new Event
{
EventType = "output",
Body = new OutputEventBody
{
Category = "stdout",
Output = maskedData + "\n"
}
});
}
};
Apply the same change to ErrorDataReceived handler (~lines 696-712).
Change 2: Return Only Exit Code in Result
Location: Lines ~767-772 (return statement in ExecuteShellCommandAsync)
Before:
return new EvaluateResponseBody
{
Result = result.TrimEnd('\r', '\n'),
Type = exitCode == 0 ? "string" : "error",
VariablesReference = 0
};
After:
return new EvaluateResponseBody
{
Result = $"(exit code: {exitCode})",
Type = exitCode == 0 ? "string" : "error",
VariablesReference = 0
};
Also remove the result combination logic (lines ~747-762) since we no longer need to build the full result string for the response.
Change 3: Fix Expression vs Shell Routing
Location: Lines ~511-536 (HandleEvaluateAsync method)
Before:
try
{
// Check if this is a REPL/shell command (context: "repl") or starts with shell prefix
if (evalContext == "repl" || expression.StartsWith("!") || expression.StartsWith("$"))
{
// Shell execution mode
var command = expression.TrimStart('!', '$').Trim();
// ...
}
else
{
// Expression evaluation mode
var result = EvaluateExpression(expression, executionContext);
return CreateSuccessResponse(result);
}
}
After:
try
{
// GitHub Actions expressions start with "${{" - always evaluate as expressions
if (expression.StartsWith("${{"))
{
var result = EvaluateExpression(expression, executionContext);
return CreateSuccessResponse(result);
}
// Check if this is a REPL/shell command:
// - context is "repl" (from Debug Console pane)
// - expression starts with "!" (explicit shell prefix for Watch pane)
if (evalContext == "repl" || expression.StartsWith("!"))
{
// Shell execution mode
var command = expression.TrimStart('!').Trim();
if (string.IsNullOrEmpty(command))
{
return CreateSuccessResponse(new EvaluateResponseBody
{
Result = "(empty command)",
Type = "string",
VariablesReference = 0
});
}
var result = await ExecuteShellCommandAsync(command, executionContext);
return CreateSuccessResponse(result);
}
else
{
// Expression evaluation mode (Watch pane, hover, etc.)
var result = EvaluateExpression(expression, executionContext);
return CreateSuccessResponse(result);
}
}
Change 4: Add Expression Expansion Helper Method
Location: Add new method before ExecuteShellCommandAsync (~line 667)
/// <summary>
/// Expands ${{ }} expressions within a command string.
/// For example: "echo ${{github.event_name}}" -> "echo push"
/// </summary>
private string ExpandExpressionsInCommand(string command, IExecutionContext context)
{
if (string.IsNullOrEmpty(command) || !command.Contains("${{"))
{
return command;
}
try
{
// Create a StringToken with the command
var token = new StringToken(null, null, null, command);
// Use the template evaluator to expand expressions
var templateEvaluator = context.ToPipelineTemplateEvaluator();
var result = templateEvaluator.EvaluateStepDisplayName(
token,
context.ExpressionValues,
context.ExpressionFunctions);
// Mask secrets in the expanded command
result = HostContext.SecretMasker.MaskSecrets(result ?? command);
Trace.Info($"Expanded command: {result}");
return result;
}
catch (Exception ex)
{
Trace.Info($"Expression expansion failed, using original command: {ex.Message}");
return command;
}
}
Required import: Add using GitHub.DistributedTask.ObjectTemplating.Tokens; at the top of the file if not already present.
Change 5: Use Expression Expansion in Shell Execution
Location: Beginning of ExecuteShellCommandAsync method (~line 670)
Before:
private async Task<EvaluateResponseBody> ExecuteShellCommandAsync(string command, IExecutionContext context)
{
Trace.Info($"Executing shell command: {command}");
// ...
}
After:
private async Task<EvaluateResponseBody> ExecuteShellCommandAsync(string command, IExecutionContext context)
{
// Expand ${{ }} expressions in the command first
command = ExpandExpressionsInCommand(command, context);
Trace.Info($"Executing shell command: {command}");
// ...
}
DAP Context Reference
For future reference, these are the DAP evaluate context values:
| DAP Context | Source UI | Behavior |
|---|---|---|
"repl" |
Debug Console / REPL pane | Shell execution (with expression expansion) |
"watch" |
Watch / Expressions pane | Expression evaluation |
"hover" |
Editor hover (default) | Expression evaluation |
"variables" |
Variables pane | Expression evaluation |
"clipboard" |
Copy to clipboard | Expression evaluation |
Testing Checklist
- REPL command output is masked and appears only once
- REPL command shows exit code in result field
- Expression
${{github.event_name}}evaluates correctly in Watch pane - Expression
${{github.event_name}} == 'push'evaluates correctly - REPL command
echo ${{github.event_name}}expands and executes correctly - REPL command
!ls -lafrom Watch pane works (explicit shell prefix) - Secrets are masked in all outputs (streaming and expanded commands)