mirror of
https://github.com/actions/runner.git
synced 2026-01-19 10:46:27 +08:00
Phase 2 done
This commit is contained in:
@@ -4,6 +4,14 @@
|
|||||||
**Author:** GitHub Actions Team
|
**Author:** GitHub Actions Team
|
||||||
**Date:** January 2026
|
**Date:** January 2026
|
||||||
|
|
||||||
|
## Progress Checklist
|
||||||
|
|
||||||
|
- [x] **Phase 1:** DAP Protocol Infrastructure (DapMessages.cs, DapServer.cs, basic DapDebugSession.cs)
|
||||||
|
- [x] **Phase 2:** Debug Session Logic (DapVariableProvider.cs, variable inspection, step history tracking)
|
||||||
|
- [ ] **Phase 3:** StepsRunner Integration (pause hooks before/after step execution)
|
||||||
|
- [ ] **Phase 4:** Expression Evaluation & Shell (REPL)
|
||||||
|
- [ ] **Phase 5:** Startup Integration (JobRunner.cs modifications)
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
This document describes the implementation of Debug Adapter Protocol (DAP) support in the GitHub Actions runner, enabling rich debugging of workflow jobs from any DAP-compatible editor (nvim-dap, VS Code, etc.).
|
This document describes the implementation of Debug Adapter Protocol (DAP) support in the GitHub Actions runner, enabling rich debugging of workflow jobs from any DAP-compatible editor (nvim-dap, VS Code, etc.).
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using GitHub.DistributedTask.WebApi;
|
using GitHub.DistributedTask.WebApi;
|
||||||
@@ -81,6 +82,16 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
public const string Exception = "exception";
|
public const string Exception = "exception";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores information about a completed step for stack trace display.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class CompletedStepInfo
|
||||||
|
{
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
public TaskResult? Result { get; set; }
|
||||||
|
public int FrameId { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Interface for the DAP debug session.
|
/// Interface for the DAP debug session.
|
||||||
/// Handles debug state, step coordination, and DAP request processing.
|
/// Handles debug state, step coordination, and DAP request processing.
|
||||||
@@ -142,9 +153,12 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
// Thread ID for the single job execution thread
|
// Thread ID for the single job execution thread
|
||||||
private const int JobThreadId = 1;
|
private const int JobThreadId = 1;
|
||||||
|
|
||||||
// Frame ID for the current step
|
// Frame ID base for the current step (always 1)
|
||||||
private const int CurrentFrameId = 1;
|
private const int CurrentFrameId = 1;
|
||||||
|
|
||||||
|
// Frame IDs for completed steps start at 1000
|
||||||
|
private const int CompletedFrameIdBase = 1000;
|
||||||
|
|
||||||
private IDapServer _server;
|
private IDapServer _server;
|
||||||
private DapSessionState _state = DapSessionState.WaitingForConnection;
|
private DapSessionState _state = DapSessionState.WaitingForConnection;
|
||||||
private InitializeRequestArguments _clientCapabilities;
|
private InitializeRequestArguments _clientCapabilities;
|
||||||
@@ -153,10 +167,20 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
private TaskCompletionSource<DapCommand> _commandTcs;
|
private TaskCompletionSource<DapCommand> _commandTcs;
|
||||||
private readonly object _stateLock = new object();
|
private readonly object _stateLock = new object();
|
||||||
|
|
||||||
|
// Whether to pause before the next step (set by 'next' command)
|
||||||
|
private bool _pauseOnNextStep = true;
|
||||||
|
|
||||||
// Current execution context (set during OnStepStartingAsync)
|
// Current execution context (set during OnStepStartingAsync)
|
||||||
private IStep _currentStep;
|
private IStep _currentStep;
|
||||||
private IExecutionContext _jobContext;
|
private IExecutionContext _jobContext;
|
||||||
|
|
||||||
|
// Track completed steps for stack trace
|
||||||
|
private readonly List<CompletedStepInfo> _completedSteps = new List<CompletedStepInfo>();
|
||||||
|
private int _nextCompletedFrameId = CompletedFrameIdBase;
|
||||||
|
|
||||||
|
// Variable provider for converting contexts to DAP variables
|
||||||
|
private DapVariableProvider _variableProvider;
|
||||||
|
|
||||||
public bool IsActive => _state == DapSessionState.Ready || _state == DapSessionState.Paused || _state == DapSessionState.Running;
|
public bool IsActive => _state == DapSessionState.Ready || _state == DapSessionState.Paused || _state == DapSessionState.Running;
|
||||||
|
|
||||||
public DapSessionState State => _state;
|
public DapSessionState State => _state;
|
||||||
@@ -164,6 +188,7 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
public override void Initialize(IHostContext hostContext)
|
public override void Initialize(IHostContext hostContext)
|
||||||
{
|
{
|
||||||
base.Initialize(hostContext);
|
base.Initialize(hostContext);
|
||||||
|
_variableProvider = new DapVariableProvider(hostContext);
|
||||||
Trace.Info("DapDebugSession initialized");
|
Trace.Info("DapDebugSession initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,7 +324,7 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
// We have a single thread representing the job execution
|
// We have a single thread representing the job execution
|
||||||
var body = new ThreadsResponseBody
|
var body = new ThreadsResponseBody
|
||||||
{
|
{
|
||||||
Threads = new System.Collections.Generic.List<Thread>
|
Threads = new List<Thread>
|
||||||
{
|
{
|
||||||
new Thread
|
new Thread
|
||||||
{
|
{
|
||||||
@@ -318,15 +343,19 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
{
|
{
|
||||||
var args = request.Arguments?.ToObject<StackTraceArguments>();
|
var args = request.Arguments?.ToObject<StackTraceArguments>();
|
||||||
|
|
||||||
var frames = new System.Collections.Generic.List<StackFrame>();
|
var frames = new List<StackFrame>();
|
||||||
|
|
||||||
// Add current step as the top frame
|
// Add current step as the top frame
|
||||||
if (_currentStep != null)
|
if (_currentStep != null)
|
||||||
{
|
{
|
||||||
|
var resultIndicator = _currentStep.ExecutionContext?.Result != null
|
||||||
|
? $" [{_currentStep.ExecutionContext.Result}]"
|
||||||
|
: " [running]";
|
||||||
|
|
||||||
frames.Add(new StackFrame
|
frames.Add(new StackFrame
|
||||||
{
|
{
|
||||||
Id = CurrentFrameId,
|
Id = CurrentFrameId,
|
||||||
Name = _currentStep.DisplayName ?? "Current Step",
|
Name = $"{_currentStep.DisplayName ?? "Current Step"}{resultIndicator}",
|
||||||
Line = 1,
|
Line = 1,
|
||||||
Column = 1,
|
Column = 1,
|
||||||
PresentationHint = "normal"
|
PresentationHint = "normal"
|
||||||
@@ -344,7 +373,20 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: In Phase 2, add completed steps as additional frames
|
// Add completed steps as additional frames (most recent first)
|
||||||
|
for (int i = _completedSteps.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var completedStep = _completedSteps[i];
|
||||||
|
var resultStr = completedStep.Result.HasValue ? $" [{completedStep.Result}]" : "";
|
||||||
|
frames.Add(new StackFrame
|
||||||
|
{
|
||||||
|
Id = completedStep.FrameId,
|
||||||
|
Name = $"{completedStep.DisplayName}{resultStr}",
|
||||||
|
Line = 1,
|
||||||
|
Column = 1,
|
||||||
|
PresentationHint = "subtle"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var body = new StackTraceResponseBody
|
var body = new StackTraceResponseBody
|
||||||
{
|
{
|
||||||
@@ -357,44 +399,39 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
|
|
||||||
private Response HandleScopes(Request request)
|
private Response HandleScopes(Request request)
|
||||||
{
|
{
|
||||||
// Stub implementation - Phase 2 will populate with actual contexts
|
var args = request.Arguments?.ToObject<ScopesArguments>();
|
||||||
var body = new ScopesResponseBody
|
var frameId = args?.FrameId ?? CurrentFrameId;
|
||||||
{
|
|
||||||
Scopes = new System.Collections.Generic.List<Scope>
|
|
||||||
{
|
|
||||||
new Scope { Name = "github", VariablesReference = 1, Expensive = false },
|
|
||||||
new Scope { Name = "env", VariablesReference = 2, Expensive = false },
|
|
||||||
new Scope { Name = "runner", VariablesReference = 3, Expensive = false },
|
|
||||||
new Scope { Name = "job", VariablesReference = 4, Expensive = false },
|
|
||||||
new Scope { Name = "steps", VariablesReference = 5, Expensive = false },
|
|
||||||
new Scope { Name = "secrets", VariablesReference = 6, Expensive = false, PresentationHint = "registers" },
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return CreateSuccessResponse(body);
|
// Get the execution context for the requested frame
|
||||||
|
var context = GetExecutionContextForFrame(frameId);
|
||||||
|
if (context == null)
|
||||||
|
{
|
||||||
|
// Return empty scopes if no context available
|
||||||
|
return CreateSuccessResponse(new ScopesResponseBody { Scopes = new List<Scope>() });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the variable provider to get scopes
|
||||||
|
var scopes = _variableProvider.GetScopes(context, frameId);
|
||||||
|
|
||||||
|
return CreateSuccessResponse(new ScopesResponseBody { Scopes = scopes });
|
||||||
}
|
}
|
||||||
|
|
||||||
private Response HandleVariables(Request request)
|
private Response HandleVariables(Request request)
|
||||||
{
|
{
|
||||||
// Stub implementation - Phase 2 will populate with actual variable values
|
|
||||||
var args = request.Arguments?.ToObject<VariablesArguments>();
|
var args = request.Arguments?.ToObject<VariablesArguments>();
|
||||||
var variablesRef = args?.VariablesReference ?? 0;
|
var variablesRef = args?.VariablesReference ?? 0;
|
||||||
|
|
||||||
var body = new VariablesResponseBody
|
// Get the current execution context
|
||||||
|
var context = _currentStep?.ExecutionContext ?? _jobContext;
|
||||||
|
if (context == null)
|
||||||
{
|
{
|
||||||
Variables = new System.Collections.Generic.List<Variable>
|
return CreateSuccessResponse(new VariablesResponseBody { Variables = new List<Variable>() });
|
||||||
{
|
}
|
||||||
new Variable
|
|
||||||
{
|
|
||||||
Name = "(stub)",
|
|
||||||
Value = $"Variables for scope {variablesRef} will be implemented in Phase 2",
|
|
||||||
Type = "string",
|
|
||||||
VariablesReference = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return CreateSuccessResponse(body);
|
// Use the variable provider to get variables
|
||||||
|
var variables = _variableProvider.GetVariables(context, variablesRef);
|
||||||
|
|
||||||
|
return CreateSuccessResponse(new VariablesResponseBody { Variables = variables });
|
||||||
}
|
}
|
||||||
|
|
||||||
private Response HandleContinue(Request request)
|
private Response HandleContinue(Request request)
|
||||||
@@ -406,6 +443,7 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
if (_state == DapSessionState.Paused)
|
if (_state == DapSessionState.Paused)
|
||||||
{
|
{
|
||||||
_state = DapSessionState.Running;
|
_state = DapSessionState.Running;
|
||||||
|
_pauseOnNextStep = false; // Don't pause on next step
|
||||||
_commandTcs?.TrySetResult(DapCommand.Continue);
|
_commandTcs?.TrySetResult(DapCommand.Continue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -425,6 +463,7 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
if (_state == DapSessionState.Paused)
|
if (_state == DapSessionState.Paused)
|
||||||
{
|
{
|
||||||
_state = DapSessionState.Running;
|
_state = DapSessionState.Running;
|
||||||
|
_pauseOnNextStep = true; // Pause before next step
|
||||||
_commandTcs?.TrySetResult(DapCommand.Next);
|
_commandTcs?.TrySetResult(DapCommand.Next);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,13 +478,13 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
// The runner will pause at the next step boundary
|
// The runner will pause at the next step boundary
|
||||||
lock (_stateLock)
|
lock (_stateLock)
|
||||||
{
|
{
|
||||||
// Just acknowledge - actual pause happens at step boundary
|
_pauseOnNextStep = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return CreateSuccessResponse(null);
|
return CreateSuccessResponse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<Response> HandleEvaluateAsync(Request request)
|
private Task<Response> HandleEvaluateAsync(Request request)
|
||||||
{
|
{
|
||||||
var args = request.Arguments?.ToObject<EvaluateArguments>();
|
var args = request.Arguments?.ToObject<EvaluateArguments>();
|
||||||
var expression = args?.Expression ?? "";
|
var expression = args?.Expression ?? "";
|
||||||
@@ -461,7 +500,7 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
VariablesReference = 0
|
VariablesReference = 0
|
||||||
};
|
};
|
||||||
|
|
||||||
return CreateSuccessResponse(body);
|
return Task.FromResult(CreateSuccessResponse(body));
|
||||||
}
|
}
|
||||||
|
|
||||||
private Response HandleSetBreakpoints(Request request)
|
private Response HandleSetBreakpoints(Request request)
|
||||||
@@ -500,6 +539,18 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
_currentStep = step;
|
_currentStep = step;
|
||||||
_jobContext = jobContext;
|
_jobContext = jobContext;
|
||||||
|
|
||||||
|
// Reset variable provider state for new step context
|
||||||
|
_variableProvider.Reset();
|
||||||
|
|
||||||
|
// Determine if we should pause
|
||||||
|
bool shouldPause = isFirstStep || _pauseOnNextStep;
|
||||||
|
|
||||||
|
if (!shouldPause)
|
||||||
|
{
|
||||||
|
Trace.Info($"Step starting (not pausing): {step.DisplayName}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var reason = isFirstStep ? StopReason.Entry : StopReason.Step;
|
var reason = isFirstStep ? StopReason.Entry : StopReason.Step;
|
||||||
var description = isFirstStep
|
var description = isFirstStep
|
||||||
? $"Stopped at job entry: {step.DisplayName}"
|
? $"Stopped at job entry: {step.DisplayName}"
|
||||||
@@ -531,10 +582,19 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Trace.Info($"Step completed: {step.DisplayName}, result: {step.ExecutionContext?.Result}");
|
var result = step.ExecutionContext?.Result;
|
||||||
|
Trace.Info($"Step completed: {step.DisplayName}, result: {result}");
|
||||||
|
|
||||||
// The step context will be available for inspection
|
// Add to completed steps list
|
||||||
// Future: could pause here if "pause after step" is enabled
|
_completedSteps.Add(new CompletedStepInfo
|
||||||
|
{
|
||||||
|
DisplayName = step.DisplayName,
|
||||||
|
Result = result,
|
||||||
|
FrameId = _nextCompletedFrameId++
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear current step reference since it's done
|
||||||
|
// (will be set again when next step starts)
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnJobCompleted()
|
public void OnJobCompleted()
|
||||||
@@ -559,7 +619,7 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Send exited event
|
// Send exited event
|
||||||
var exitCode = _jobContext?.Result == GitHub.DistributedTask.WebApi.TaskResult.Succeeded ? 0 : 1;
|
var exitCode = _jobContext?.Result == TaskResult.Succeeded ? 0 : 1;
|
||||||
_server?.SendEvent(new Event
|
_server?.SendEvent(new Event
|
||||||
{
|
{
|
||||||
EventType = "exited",
|
EventType = "exited",
|
||||||
@@ -607,6 +667,22 @@ namespace GitHub.Runner.Worker.Dap
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the execution context for a given frame ID.
|
||||||
|
/// Currently only supports the current frame (completed steps don't have saved contexts).
|
||||||
|
/// </summary>
|
||||||
|
private IExecutionContext GetExecutionContextForFrame(int frameId)
|
||||||
|
{
|
||||||
|
if (frameId == CurrentFrameId)
|
||||||
|
{
|
||||||
|
return _currentStep?.ExecutionContext ?? _jobContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For completed steps, we would need to save their execution contexts
|
||||||
|
// For now, return null (variables won't be available for completed steps)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Response Helpers
|
#region Response Helpers
|
||||||
|
|||||||
293
src/Runner.Worker/Dap/DapVariableProvider.cs
Normal file
293
src/Runner.Worker/Dap/DapVariableProvider.cs
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||||
|
using GitHub.Runner.Common;
|
||||||
|
|
||||||
|
namespace GitHub.Runner.Worker.Dap
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides DAP variable information from the execution context.
|
||||||
|
/// Maps workflow contexts (github, env, runner, job, steps, secrets) to DAP scopes and variables.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DapVariableProvider
|
||||||
|
{
|
||||||
|
// Well-known scope names that map to top-level contexts
|
||||||
|
private static readonly string[] ScopeNames = { "github", "env", "runner", "job", "steps", "secrets", "inputs", "vars", "matrix", "needs" };
|
||||||
|
|
||||||
|
// Reserved variable reference ranges for scopes (1-100)
|
||||||
|
private const int ScopeReferenceBase = 1;
|
||||||
|
private const int ScopeReferenceMax = 100;
|
||||||
|
|
||||||
|
// Dynamic variable references start after scope range
|
||||||
|
private const int DynamicReferenceBase = 101;
|
||||||
|
|
||||||
|
private readonly IHostContext _hostContext;
|
||||||
|
private readonly Dictionary<int, (PipelineContextData Data, string Path)> _variableReferences = new();
|
||||||
|
private int _nextVariableReference = DynamicReferenceBase;
|
||||||
|
|
||||||
|
public DapVariableProvider(IHostContext hostContext)
|
||||||
|
{
|
||||||
|
_hostContext = hostContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the variable reference state. Call this when the execution context changes.
|
||||||
|
/// </summary>
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
_variableReferences.Clear();
|
||||||
|
_nextVariableReference = DynamicReferenceBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of scopes for a given execution context.
|
||||||
|
/// Each scope represents a top-level context like 'github', 'env', etc.
|
||||||
|
/// </summary>
|
||||||
|
public List<Scope> GetScopes(IExecutionContext context, int frameId)
|
||||||
|
{
|
||||||
|
var scopes = new List<Scope>();
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
var variablesRef = ScopeReferenceBase + i;
|
||||||
|
var scope = new Scope
|
||||||
|
{
|
||||||
|
Name = scopeName,
|
||||||
|
VariablesReference = variablesRef,
|
||||||
|
Expensive = false,
|
||||||
|
// Secrets get a special presentation hint
|
||||||
|
PresentationHint = scopeName == "secrets" ? "registers" : null
|
||||||
|
};
|
||||||
|
|
||||||
|
// Count named variables if it's a dictionary
|
||||||
|
if (value is DictionaryContextData dict)
|
||||||
|
{
|
||||||
|
scope.NamedVariables = dict.Count;
|
||||||
|
}
|
||||||
|
else if (value is CaseSensitiveDictionaryContextData csDict)
|
||||||
|
{
|
||||||
|
scope.NamedVariables = csDict.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
scopes.Add(scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets variables for a given variable reference.
|
||||||
|
/// </summary>
|
||||||
|
public List<Variable> GetVariables(IExecutionContext context, int variablesReference)
|
||||||
|
{
|
||||||
|
var variables = new List<Variable>();
|
||||||
|
|
||||||
|
if (context?.ExpressionValues == null)
|
||||||
|
{
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
PipelineContextData data = null;
|
||||||
|
string basePath = null;
|
||||||
|
bool isSecretsScope = false;
|
||||||
|
|
||||||
|
// Check if this is a scope reference (1-100)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check dynamic references
|
||||||
|
else if (_variableReferences.TryGetValue(variablesReference, out var refData))
|
||||||
|
{
|
||||||
|
data = refData.Data;
|
||||||
|
basePath = refData.Path;
|
||||||
|
// Check if we're inside the secrets scope
|
||||||
|
isSecretsScope = basePath?.StartsWith("secrets", StringComparison.OrdinalIgnoreCase) == true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data == null)
|
||||||
|
{
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the data to variables
|
||||||
|
ConvertToVariables(data, basePath, isSecretsScope, variables);
|
||||||
|
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts PipelineContextData to DAP Variable objects.
|
||||||
|
/// </summary>
|
||||||
|
private void ConvertToVariables(PipelineContextData data, string basePath, bool isSecretsScope, List<Variable> variables)
|
||||||
|
{
|
||||||
|
switch (data)
|
||||||
|
{
|
||||||
|
case DictionaryContextData dict:
|
||||||
|
ConvertDictionaryToVariables(dict, basePath, isSecretsScope, variables);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case CaseSensitiveDictionaryContextData csDict:
|
||||||
|
ConvertCaseSensitiveDictionaryToVariables(csDict, basePath, isSecretsScope, variables);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ArrayContextData array:
|
||||||
|
ConvertArrayToVariables(array, basePath, isSecretsScope, variables);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Scalar value - shouldn't typically get here for a container
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConvertDictionaryToVariables(DictionaryContextData dict, string basePath, bool isSecretsScope, List<Variable> variables)
|
||||||
|
{
|
||||||
|
foreach (var pair in dict)
|
||||||
|
{
|
||||||
|
var variable = CreateVariable(pair.Key, pair.Value, basePath, isSecretsScope);
|
||||||
|
variables.Add(variable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConvertCaseSensitiveDictionaryToVariables(CaseSensitiveDictionaryContextData dict, string basePath, bool isSecretsScope, List<Variable> variables)
|
||||||
|
{
|
||||||
|
foreach (var pair in dict)
|
||||||
|
{
|
||||||
|
var variable = CreateVariable(pair.Key, pair.Value, basePath, isSecretsScope);
|
||||||
|
variables.Add(variable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ConvertArrayToVariables(ArrayContextData array, string basePath, bool isSecretsScope, List<Variable> variables)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < array.Count; i++)
|
||||||
|
{
|
||||||
|
var item = array[i];
|
||||||
|
var variable = CreateVariable($"[{i}]", item, basePath, isSecretsScope);
|
||||||
|
variable.Name = $"[{i}]";
|
||||||
|
variables.Add(variable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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} }}}}"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (value == null)
|
||||||
|
{
|
||||||
|
variable.Value = "null";
|
||||||
|
variable.Type = "null";
|
||||||
|
variable.VariablesReference = 0;
|
||||||
|
return variable;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (value)
|
||||||
|
{
|
||||||
|
case StringContextData str:
|
||||||
|
if (isSecretsScope)
|
||||||
|
{
|
||||||
|
// Always mask secrets regardless of value
|
||||||
|
variable.Value = "[REDACTED]";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Mask any secret values that might be in non-secret contexts
|
||||||
|
variable.Value = MaskSecrets(str.Value);
|
||||||
|
}
|
||||||
|
variable.Type = "string";
|
||||||
|
variable.VariablesReference = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case NumberContextData num:
|
||||||
|
variable.Value = num.ToString();
|
||||||
|
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:
|
||||||
|
// Unknown type - convert to string representation
|
||||||
|
var rawValue = value.ToJToken()?.ToString() ?? "unknown";
|
||||||
|
variable.Value = MaskSecrets(rawValue);
|
||||||
|
variable.Type = value.GetType().Name;
|
||||||
|
variable.VariablesReference = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return variable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a nested variable reference and returns its ID.
|
||||||
|
/// </summary>
|
||||||
|
private int RegisterVariableReference(PipelineContextData data, string path)
|
||||||
|
{
|
||||||
|
var reference = _nextVariableReference++;
|
||||||
|
_variableReferences[reference] = (data, path);
|
||||||
|
return reference;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Masks any secret values in the string using the host context's secret masker.
|
||||||
|
/// </summary>
|
||||||
|
private string MaskSecrets(string value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
return value ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _hostContext.SecretMasker.MaskSecrets(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user