When the debugger pauses after job completion, surface the synthetic
"Complete job" entry as the active stack frame so clients highlight
the cleanup line. Previously the position stayed on the last real step.
Threads CompleteJobLine through RenderResult / JobExecutionView and
gates HandleStackTrace on a new _jobCompleted flag set when
OnJobCompletedAsync enters its inspection pause.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Follow-up to the foundation simplification: the DAP integration no
longer needs to forward Main-step skips to the view. Predicted Post
placeholders for skipped Main steps simply stay in the view as
informational entries; the debugger never pauses on them because the
IStep→line mapping is only populated when a placeholder is claimed.
Drop:
- the `TryMarkSkipped` branch in `DapDebugger.OnStepCompleted`
(the rest of the method — clearing the current-step ref — stays).
- the two skip-path `OnStepCompleted` calls in `StepsRunner`. The
surviving non-skip call is pre-existing pause-after handling.
- the lifecycle test for the dropped behaviour.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wires three runner-internal call sites into the DAP debugger:
- JobRunner: notifies the debugger once Pre/Main/Post step lists
are populated, so it can seed the execution view and predict
Post placeholders.
- ExecutionContext.RegisterPostJobStep: notifies the debugger when
a post step is registered, so it can claim the predicted
placeholder or append a new entry.
- StepsRunner: notifies the debugger when a Main step is skipped
by its if: condition, so predicted Post placeholders for that
step can be marked as skipped.
Each call site gates on jobContext.Global.Debugger?.Enabled == true
before calling HostContext.GetService<IDapDebugger>(). Without the
gate, GetService<T>() auto-instantiates the default singleton for
every non-debug job, breaking the 'no debugger, no risk' containment
property.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Adds DAP source, stackTrace, and loadedSources handlers backed by a
synthesized JobExecutionView. The view is served via sourceReference,
with a synthetic .yml path so clients pick YAML syntax highlighting.
User-visible text (path, name, content) is run through the secret
masker.
Adds three lifecycle methods to IDapDebugger:
- OnJobStepsInitializedAsync seeds the view and synthesizes Post
placeholders predictively from action Pre/Post metadata, so VS
Code's sourceReference cache hits a stable view.
- OnPostStepRegistered claims a predicted placeholder when the real
Post IActionRunner is registered, or appends a new entry.
- OnStepCompleted marks predicted Post placeholders as skipped when
their Main step is skipped.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Bridges the runner's IStep / IActionRunner types to the renderer's
JobExecutionViewEntry (#PR1b). Given a runtime step, produces the
data the renderer needs to emit one entry in the execution view.
Specifically:
- Determines the entry's phase from ActionRunStage / IStep type.
- Filters JobExtensionRunner and other non-IActionRunner steps:
those represent runner-internal scaffolding, not user-visible
steps.
- Filters auto-generated step IDs (regex against `^__\d+$` and
GUID-shaped strings) so only explicit `id:` fields surface.
- Serializes `with:` and `env:` via TemplateTokenYamlAdapter
(#PR1d) so `${{ ... }}` expressions are preserved verbatim in
the rendered source.
- Extracts `run:`, `shell:`, `working-directory:` from a script
step's `Inputs` map using the constants defined in
PipelineConstants.ScriptStepInputs (the runner stores these as
camelCase `workingDirectory`, not the kebab-case spelling from
workflow YAML).
This is part 5 of 5 splitting the previously-monolithic foundation.
The DAP-integration PR wires this into JobRunner / ExecutionContext
so steps actually flow into the execution view at runtime.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
A step's parameters (`with:`, `env:`, `if:`, ...) arrive at the
runner as TemplateToken trees with `${{ ... }}` expressions still
embedded. The DAP execution view (the source the debugger serves)
must reflect those parameters as the user authored them — pre
evaluation, with expressions intact — so that what the user sees in
their debugger matches their workflow file.
This commit adds a YamlDotNet `IObjectWriter` adapter so the
runner's existing `TemplateWriter.Write` can drive a YamlDotNet
`Emitter`. With the adapter, serializing a TemplateToken tree to
YAML is a single call. The adapter walks BasicExpressionTokens via
`ToDisplayString()` instead of `ToString()` so that composite
scalars like `${{ runner.os }}-primes` round-trip to their authored
form (the parser otherwise rewrites them as
`format('{0}-primes', runner.os)`).
This piece is independent of the renderer (#PR1b) and view
container (#PR1c) and stacks on those PRs only for branch ordering.
The translator (#PR1e, next) is its only consumer.
This is part 4 of 5 splitting the previously-monolithic foundation.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The DAP debugger needs to map a runtime IStep back to a source line
when answering `stackTrace` requests. The renderer (#PR1b) produces
the YAML and per-entry start lines from an immutable list, but the
debugger's view grows over the job's lifetime: post steps register
lazily, and the integration layer needs O(1) IStep -> line lookup
at every pause.
This commit adds JobExecutionView, a stateful append-only wrapper
around the renderer. It maintains:
- the current entry list,
- the most recent rendered YAML,
- a Dictionary<IStep, int> for fast line lookup.
Each Append can register an entry in one of three modes:
- with a stepIdentity: registers the IStep -> line mapping
immediately;
- with a matchKey: registers an unclaimed placeholder that a
later TryClaim binds to a real IStep (used when an entry is
predicted before the runner materializes its IStep, e.g. a
Post-step placeholder synthesized at job-init from an action's
metadata);
- with neither: a static informational entry that needs no line
lookup.
This is part 3 of 5. The DAP-integration PR that consumes this
container is the final follow-up.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The DAP debugger serves a synthesized YAML document as the job's
`source`. That document is a 1:1 representation of how the runner
sees the job — not the workflow file — so pre and post action steps
appear as their own 'lines' that the user can pause on (and
eventually breakpoint, set in a follow-up PR).
This commit adds the core rendering algorithm: given a list of
phase-tagged entries (`JobExecutionViewEntry`), produce the
phase-keyed YAML plus a parallel array of 1-based line numbers
pointing at each entry's `- step:` key. The line numbers are what
later powers the DAP `stackTrace` handler.
Why hand-emit the skeleton instead of serializing a DTO?
Per-entry line offsets must be tracked at emission time. Using a
generic YAML serializer would force a second pass to scan the
output for `- step:` lines, which is fragile and breaks the moment
indentation conventions shift. Scalar values still go through the
library (via YamlScalarFormatter from #PR1a), so we don't carry
quoting rules.
Example output for a typical job (build, build, post step):
# Job: build
# Runner execution plan — read-only.
setup:
- step: Setup job
main:
- step: Run actions/checkout@v6
uses: actions/checkout@v6
if: success()
- step: Cache Primes
id: cache-primes
uses: actions/cache@v5
if: success()
with:
path: prime-numbers
key: ${{ runner.os }}-primes
post:
- step: Post Cache Primes
action: actions/cache@v5
cleanup:
- step: Complete job
This is part 2 of 5 splitting the previously-monolithic foundation
for review tractability. The wiring that turns runner state into
these entries lives in the next PRs.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Remove the redundant second `TrimEnd('\n')` from the return path.
The earlier trim already removes any trailing newline before the
`\n...` doc-end check; the marker-removal substring does not
re-introduce one, so the second trim was dead code.
- Surface full exception (`ex.ToString()`) in the test round-trip
helper so YAML parse failures show stack + inner exception, not
just the top-level message.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The upcoming DAP execution-view renderer serves a synthesized YAML
document as the job's debugger source. The skeleton is hand-emitted
so we can track per-step line offsets, but scalar values (step names,
action refs, etc.) need quote-safe formatting that respects YAML's
reserved chars, leading/trailing whitespace, and embedded `: `/`#`
sequences. Doing this by hand is bug-prone and easy to get wrong on
edge cases (empty strings, expressions, multiline content).
This commit adds a thin wrapper around YamlDotNet's `Emitter` that
emits a single scalar, strips the surrounding document markers, and
forces LF line breaks (`StringWriter` otherwise picks up Windows's
CRLF via `Environment.NewLine` and corrupts the document-end
stripping).
No caller yet — the renderer that uses it lands in a follow-up PR.
This is part 1 of 5 splitting the previously-monolithic foundation
for review tractability.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>