Skip to content

DAP: add JobExecutionView model + renderer#4417

Open
rentziass wants to merge 8 commits into
mainfrom
dap-execution-view-foundation
Open

DAP: add JobExecutionView model + renderer#4417
rentziass wants to merge 8 commits into
mainfrom
dap-execution-view-foundation

Conversation

@rentziass
Copy link
Copy Markdown
Member

@rentziass rentziass commented May 13, 2026

This is part 1 of 2 of the work to serve the job "source" during debugging.

In this PR is all the code to build that representation: this is intended to be a 1:1 representation of how the runner sees the job it's executing rendered as YAML, it's not the workflow file the job comes from; this allows us to consider pre and post actions steps as "lines" we can pause/add breakpoints (coming next) and show things exactly as the runner interprets them. We use YamlDotNet to render scalar values but we "hand-write" the source skeleton because we need to keep track of each step's line to be able to refer to it later (and to write comments but that's secondary at the moment).

Example YAML served as source
# 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
  - step: Check prerequisites
    run: |
      echo banana
      echo "Checking system requirements..."
      echo "Node version: $(node --version 2>/dev/null || echo 'not installed')"
      echo "Python version: $(python3 --version 2>/dev/null || echo 'not installed')"
    if: success()
  - step: Install dependencies
    run: |
      cat > package.json << 'EOF'
      {
        "name": "demo",
        "scripts": {
          "build": "node build.js",
          "test": "node test.js"
        }
      }
      EOF
      cat > build.js << 'EOF'
      const fs = require('fs');
      const config = require('./config.json');
      fs.writeFileSync('dist/output.js', `// Built for ${config.env}`);
      EOF
    if: success()
  - step: Build
    run: |
      mkdir -p dist
      npm run build
    if: success()
  - step: Verify build
    run: |
      test -f dist/output.js && echo "Build successful!"
    if: success()

post:
  - step: Post Cache Primes
    action: actions/cache@v5
  - step: Post Run actions/checkout@v6
    action: actions/checkout@v6

cleanup:
  - step: Complete job

rentziass and others added 6 commits May 12, 2026 13:26
Pure renderer that emits a phase-keyed YAML view (setup/pre/main/
post/cleanup) of a job's runner execution plan. Tracks per-entry
start lines so the debugger can map IStep instances back to source
locations. Includes a skipped-step annotation for predicted Post
placeholders whose Main step never executes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Thread-safe append-only container of JobExecutionViewEntry. Supports
predictive Post-step placeholders via TryClaim(matchKey, step) and
TryMarkSkipped(matchKey) so the rendered view can reflect Main steps
that are skipped by their if: condition. Provides line lookup by
IStep for DAP stack-trace frame construction.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
IObjectWriter that bridges the runner's TemplateWriter to YamlDotNet
so TemplateToken trees can be serialized with ${{ }} expressions
preserved. Lets the execution view show step parameters (with:, env:,
if:, etc.) exactly as authored in the workflow file, before any
expression evaluation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Translates IStep instances (ActionRunner, JobExtensionRunner, etc.)
into JobExecutionViewEntry. Filters auto-generated step IDs so only
explicit IDs surface in the view, and emits step parameters via the
TemplateTokenYamlAdapter to preserve pre-evaluation expressions.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The workflow parser tokenizes a mixed scalar like
`${{ runner.os }}-primes` as a single BasicExpressionToken whose
internal expression is `format('{0}-primes', runner.os)`. The
adapter was driving TemplateWriter, which calls `ToString()` and
surfaces the parser's normalized rewrite verbatim.

Replace TemplateWriter.Write with a local walker that mirrors it
except for BasicExpressionToken — those go through
`ToDisplayString()`, reversing the format(...) rewrite back to the
authored form. All other token kinds delegate to the same writer
calls TemplateWriter would make.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
`StringWriter` defaults to `Environment.NewLine`, which is CRLF on
Windows. YamlDotNet's `Emitter` calls `WriteLine` internally, so the
emitted YAML mixes CRLF (from the emitter) with the explicit `\n` we
append in the renderer skeleton. That breaks the document-end
stripping in `FormatScalar` and `TemplateTokenYamlAdapter.Serialize`
and corrupts the rendered view on Windows.

Set `sw.NewLine = "\n"` in both emitter entry points so the output
is always LF, regardless of host. Add regression tests asserting no
`\r` appears in the rendered view or in adapter output. The tests
are noop guards on Linux (where `Environment.NewLine` is already
\n) but catch any future regression on the Windows CI matrix.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@rentziass rentziass marked this pull request as ready for review May 13, 2026 10:43
@rentziass rentziass requested a review from a team as a code owner May 13, 2026 10:43
Copilot AI review requested due to automatic review settings May 13, 2026 10:43
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces the core model + rendering pipeline for a DAP “job execution view”: translating runner steps into a pure-data representation and rendering that representation as stable, line-addressable YAML (intended to mirror how the runner will execute the job, not the original workflow file).

Changes:

  • Added JobExecutionViewEntry/JobExecutionPhase plus a deterministic YAML renderer that tracks each entry’s - step: line.
  • Added a stateful JobExecutionView container to append entries during execution and resolve IStep identity → YAML line in O(1).
  • Added StepEntryTranslator and TemplateTokenYamlAdapter to preserve author-facing expressions when emitting YAML fragments, plus L0 coverage.
Show a summary per file
File Description
src/Runner.Worker/Dap/TemplateTokenYamlAdapter.cs Adapter to serialize TemplateToken back to YAML while preserving expression display forms.
src/Runner.Worker/Dap/StepEntryTranslator.cs Translates IStep/IActionRunner into JobExecutionViewEntry data for rendering.
src/Runner.Worker/Dap/JobExecutionViewRenderer.cs Pure renderer producing execution-view YAML and per-entry start line mapping.
src/Runner.Worker/Dap/JobExecutionView.cs Stateful append-only view with cached YAML and step→line lookups, plus placeholder claiming/skipping.
src/Test/L0/Worker/TemplateTokenYamlAdapterL0.cs Unit tests for YAML serialization of TemplateToken values and expressions.
src/Test/L0/Worker/StepEntryTranslatorL0.cs Unit tests for translating runner steps into view entries.
src/Test/L0/Worker/JobExecutionViewRendererL0.cs Unit tests for rendering format, field ordering, quoting, and line number stability.
src/Test/L0/Worker/JobExecutionViewL0.cs Unit tests for append/claim/skip behavior and concurrency safety in the stateful view.

Copilot's findings

Comments suppressed due to low confidence (4)

src/Test/L0/Worker/JobExecutionViewRendererL0.cs:475

  • iStep is computed but never used, which triggers CS0219 and fails the build when warnings are treated as errors. Remove the unused local (or actually use it as part of the ordering assertions).
            int iStep = y.IndexOf("    - step: ", StringComparison.Ordinal) >= 0
                ? y.IndexOf("- step:", StringComparison.Ordinal) : y.IndexOf("- step:", StringComparison.Ordinal);
            int iId = y.IndexOf("    id:", StringComparison.Ordinal);

src/Runner.Worker/Dap/StepEntryTranslator.cs:138

  • This lookup uses "working-directory", but ActionStep.Inputs keys for script steps are workingDirectory (see PipelineConstants.ScriptStepInputs.WorkingDirectory and ScriptHandler). As-is, workingDirectory will never populate in the view. Update the key(s) used here accordingly.
                            }
                        }
                        if (TryGetMapValue(inputs, "working-directory", out var wdTok) && wdTok != null)
                        {
                            string wdText = wdTok.ToString();
                            if (!string.IsNullOrEmpty(wdText))
                            {
                                workingDirectory = wdText;
                            }
                        }

src/Test/L0/Worker/StepEntryTranslatorL0.cs:254

  • These tests use the run-step input key working-directory, but the runner’s ActionStep.Inputs uses workingDirectory (see PipelineConstants.ScriptStepInputs.WorkingDirectory). Once the translator is fixed to read the real key, update the test to match (and optionally add coverage that both spellings are accepted if you support backward compatibility).
            {
                Reference = new ScriptReference(),
                Inputs = Map(
                    ("script", Str("npm test")),
                    ("shell", Str("bash")),
                    ("working-directory", Str("./api"))),
            };

src/Test/L0/Worker/StepEntryTranslatorL0.cs:283

  • The translator’s run-step internal key filtering should match the actual input keys (script, shell, workingDirectory). This test currently uses working-directory, which doesn’t align with PipelineConstants.ScriptStepInputs.WorkingDirectory and could mask bugs in the filter logic. Update the test data/expectations to use the real key (or assert both are filtered if you intend to support both spellings).
            var action = new ActionStep
            {
                Reference = reference,
                Inputs = Map(
                    ("mode", Str("ci")),
                    ("script", Str("leak")),
                    ("shell", Str("leak")),
                    ("working-directory", Str("leak"))),
            };
            var mock = NewActionRunnerMock(ActionRunStage.Main, "Run", reference, action);
  • Files reviewed: 8/8 changed files
  • Comments generated: 5

Comment thread src/Test/L0/Worker/JobExecutionViewRendererL0.cs Outdated
Comment thread src/Test/L0/Worker/StepEntryTranslatorL0.cs Outdated
Comment thread src/Runner.Worker/Dap/StepEntryTranslator.cs
Comment thread src/Runner.Worker/Dap/TemplateTokenYamlAdapter.cs Outdated
Comment thread src/Runner.Worker/Dap/JobExecutionView.cs
rentziass and others added 2 commits May 13, 2026 03:50
Marking placeholders as skipped was a special-case treatment for
predicted Post placeholders whose Main step never executed. In practice
this is no different from any step that doesn't run: the debugger
simply never pauses on it, and the IStep→line mapping is only
populated when a placeholder is claimed.

Drop `IsSkipped` from `JobExecutionViewEntry`, the inline
`# (skipped — ...)` rendering, and `JobExecutionView.TryMarkSkipped`.
Placeholders that are never claimed simply stay in the view as
informational entries — the IStep→line dictionary excludes them,
so the debugger never tries to pause on them.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
  - Remove unused `using System.Linq;` from JobExecutionViewRendererL0
    and `using System.Collections.Generic;` from StepEntryTranslatorL0.
  - StepEntryTranslator: read run-step inputs through
    `PipelineConstants.ScriptStepInputs.*` (`script`, `shell`,
    `workingDirectory`). The runner stores these keys in their
    constants-defined spelling — see ActionManifestManagerWrapper:244 —
    not their kebab-case workflow-YAML form, so the previous
    `working-directory` lookup never matched in practice. Tests
    updated to use the same constants.
  - TemplateTokenYamlAdapter / JobExecutionViewRenderer.FormatScalar:
    defensively also strip a `---\n` prefix on top of the existing
    `--- ` prefix. Not observed in any input I exercised, but cheap
    insurance against an Emitter quirk on some YAML configurations.
  - JobExecutionView.Append: reject passing both `stepIdentity` and
    `matchKey`. The combination would orphan the original step's
    line mapping the moment TryClaim overwrites `_stepIdentities`
    for a different step. Add an L0 test covering the precondition.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants