From 25780fd9bd7d29ff2797fd94acb0c9b289c9a976 Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:49:00 -0400 Subject: [PATCH 1/2] ci: add SourceLink step-into (F11) verification (#133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds sourcelink-stepinto.yaml + fixtures — a scheduled/manual workflow that drives netcoredbg to step into TestExtractor's constructor from a scratch consumer and asserts the debugger resolves the library's real SourceLink-mapped source, not a decompiled placeholder. Complements sourcelink.yaml (PR #208, the static `dotnet sourcelink test` gate); together they satisfy #133. Ported from ETL-Transformers. Co-Authored-By: Claude Opus 4.8 --- .github/sourcelink/Directory.Build.props | 9 ++ .github/sourcelink/Directory.Build.targets | 3 + .github/sourcelink/README.md | 51 ++++++++ .github/sourcelink/consumer/Program.cs | 14 +++ .../consumer/StepIntoConsumer.csproj | 29 +++++ .github/sourcelink/verify_stepinto.py | 115 ++++++++++++++++++ .github/workflows/sourcelink-stepinto.yaml | 99 +++++++++++++++ 7 files changed, 320 insertions(+) create mode 100644 .github/sourcelink/Directory.Build.props create mode 100644 .github/sourcelink/Directory.Build.targets create mode 100644 .github/sourcelink/README.md create mode 100644 .github/sourcelink/consumer/Program.cs create mode 100644 .github/sourcelink/consumer/StepIntoConsumer.csproj create mode 100644 .github/sourcelink/verify_stepinto.py create mode 100644 .github/workflows/sourcelink-stepinto.yaml diff --git a/.github/sourcelink/Directory.Build.props b/.github/sourcelink/Directory.Build.props new file mode 100644 index 0000000..29a7d83 --- /dev/null +++ b/.github/sourcelink/Directory.Build.props @@ -0,0 +1,9 @@ + + + diff --git a/.github/sourcelink/Directory.Build.targets b/.github/sourcelink/Directory.Build.targets new file mode 100644 index 0000000..2e60f34 --- /dev/null +++ b/.github/sourcelink/Directory.Build.targets @@ -0,0 +1,3 @@ + + + diff --git a/.github/sourcelink/README.md b/.github/sourcelink/README.md new file mode 100644 index 0000000..5aad8e3 --- /dev/null +++ b/.github/sourcelink/README.md @@ -0,0 +1,51 @@ +# SourceLink step-into verification + +Fixtures for [`sourcelink-stepinto.yaml`](../workflows/sourcelink-stepinto.yaml), +which proves the *debugger* half of SourceLink end-to-end: that pressing **F11** +in a consumer steps into the library's **real source** (resolved via the +SourceLink map in the PDB), not a decompiled placeholder. + +This complements [`sourcelink.yaml`](../workflows/sourcelink.yaml) (PR #208), which +proves every document in the PDB resolves to real GitHub content via +`dotnet sourcelink test`. Together they cover the full chain F11 depends on and +satisfy #133: SourceLink map in the PDB → source-file resolution → GitHub raw-URL +fetch. + +## How it works + +1. Build [`consumer/`](consumer/) with `-c Debug -p:ContinuousIntegrationBuild=true`. + Its `ProjectReference` compiles the library with the **same SourceLink map a + released package ships** (deterministic `/_/…` source roots that map to GitHub + raw URLs), and Debug/non-optimized codegen so the step-into target isn't + reordered away. +2. Run [`verify_stepinto.py`](verify_stepinto.py) — it drives + [netcoredbg](https://github.com/Samsung/netcoredbg) over its MI interface to + break in the consumer, step into `TestExtractor`'s constructor, and assert + the resulting frame is the library's SourceLink-mapped source file with symbols + loaded. If SourceLink or the sequence points were broken the step would land + with no source and the script exits non-zero. + +## Why a ProjectReference, not the packed NuGet + +netcoredbg is the only CI-scriptable .NET debugger, and it does **not** reliably +pair a *package*-sourced assembly with its symbol-package (`.snupkg`) PDB — it +reports `symbols-loaded=0` and cannot step into it (verified during development). +A `ProjectReference` built with `ContinuousIntegrationBuild=true` produces a +**byte-identical SourceLink PDB**, so the SourceLink map under test is the same +one that ships; only the delivery path differs. The packed-package symbol/URL +resolution is covered by `sourcelink.yaml` (PR #208). + +## Files + +- `consumer/StepIntoConsumer.csproj` / `Program.cs` — the fixture consumer. The + break line is marked `STEP_INTO_TARGET`. +- `Directory.Build.props` / `.targets` — empty isolation stubs so the consumer + does **not** inherit the repo's analyzers / BannedSymbols / multi-TFM policy. +- `verify_stepinto.py` — the debugger driver (exit 0 = step-into resolved real + source). + +## Scope + +Scheduled + manual, not a PR gate — debugger automation is heavier and slightly +less deterministic than a unit test, and SourceLink's raw URLs resolve only +against a pushed commit. diff --git a/.github/sourcelink/consumer/Program.cs b/.github/sourcelink/consumer/Program.cs new file mode 100644 index 0000000..2326c64 --- /dev/null +++ b/.github/sourcelink/consumer/Program.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using Wolfgang.Etl.TestKit; + +// End-to-end SourceLink "step into" fixture. The debugger sets a breakpoint on +// the line marked STEP_INTO_TARGET and issues a step-into (the F11 a consumer +// would press). If SourceLink is intact the debugger resolves the library's real +// source (from GitHub) at the constructor below, instead of a decompiled +// placeholder. TestExtractor's constructor is a plain, non-async method, which +// makes it a clean and stable step-into target. + +IEnumerable items = new[] { 1, 2, 3 }; +var extractor = new TestExtractor(items); // STEP_INTO_TARGET +Console.WriteLine(extractor.GetType().FullName); diff --git a/.github/sourcelink/consumer/StepIntoConsumer.csproj b/.github/sourcelink/consumer/StepIntoConsumer.csproj new file mode 100644 index 0000000..7f533e3 --- /dev/null +++ b/.github/sourcelink/consumer/StepIntoConsumer.csproj @@ -0,0 +1,29 @@ + + + + + Exe + net10.0 + enable + disable + + false + portable + + ../../../src/Wolfgang.Etl.TestKit/Wolfgang.Etl.TestKit.csproj + + + + + + + diff --git a/.github/sourcelink/verify_stepinto.py b/.github/sourcelink/verify_stepinto.py new file mode 100644 index 0000000..ab76c5b --- /dev/null +++ b/.github/sourcelink/verify_stepinto.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +"""Drive netcoredbg to prove SourceLink "step into" (F11) works end-to-end. + +Sets a breakpoint in the consumer, runs to it, issues a step-into, and inspects +the resulting stack frame. Succeeds (exit 0) only if the step landed in the +expected library source file with the library's symbols loaded — i.e. a debugger +resolves real library source, not a decompiled placeholder. If SourceLink or the +symbol package were broken, the step would land with no source mapping and this +fails. + +Usage: + verify_stepinto.py +""" +import subprocess +import sys +import re +import threading +import queue +import time + +if len(sys.argv) != 5: + print(__doc__) + sys.exit(2) + +debugger, consumer_dll, break_spec, expected_src = sys.argv[1:5] + +proc = subprocess.Popen( + [debugger, "--interpreter=mi", "--", "dotnet", consumer_dll], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + text=True, + bufsize=1, +) + +events: "queue.Queue[str]" = queue.Queue() + + +def _reader(): + for line in proc.stdout: + events.put(line.rstrip("\n")) + events.put(None) + + +threading.Thread(target=_reader, daemon=True).start() + + +def send(command): + print(">>>", command, flush=True) + proc.stdin.write(command + "\n") + proc.stdin.flush() + + +def wait_for(needles, timeout=60): + deadline = time.time() + timeout + while time.time() < deadline: + try: + line = events.get(timeout=max(0.1, deadline - time.time())) + except queue.Empty: + return None + if line is None: + return None + print("dbg>", line, flush=True) + if any(n in line for n in needles): + return line + return None + + +def fail(reason): + print("RESULT=FAIL reason=" + reason, flush=True) + try: + send("-gdb-exit") + except Exception: + pass + sys.exit(1) + + +send("-break-insert " + break_spec) +wait_for(["^done", "^error"], 15) +send("-exec-run") + +# netcoredbg halts at the managed entry point first; continue to the breakpoint. +stop = wait_for(["*stopped"], 60) +if stop and 'reason="entry-point-hit"' in stop: + send("-exec-continue") + stop = wait_for(["*stopped"], 60) + +if not stop or "breakpoint-hit" not in stop: + fail("breakpoint-not-hit") + +# Step into (F11). Retry a couple of times in case the first step stays on the +# call line before descending into the callee. +resolved = None +for _ in range(4): + send("-exec-step") + stop = wait_for(["*stopped"], 30) + if not stop: + break + match = re.search(r'frame=\{[^}]*?file="([^"]+)"[^}]*?fullname="([^"]+)"', stop) + if match and match.group(1).endswith(expected_src): + resolved = (match.group(1), match.group(2), stop) + break + +if not resolved: + fail("did-not-step-into-" + expected_src) + +send("-gdb-exit") + +source_file, fullname, stop = resolved +line_match = re.search(r'line="(\d+)"', stop) +print("RESOLVED_FILE=" + source_file, flush=True) +print("RESOLVED_FULLNAME=" + fullname, flush=True) +print("RESOLVED_LINE=" + (line_match.group(1) if line_match else "?"), flush=True) +print("RESULT=PASS", flush=True) +sys.exit(0) diff --git a/.github/workflows/sourcelink-stepinto.yaml b/.github/workflows/sourcelink-stepinto.yaml new file mode 100644 index 0000000..45b8d31 --- /dev/null +++ b/.github/workflows/sourcelink-stepinto.yaml @@ -0,0 +1,99 @@ +name: SourceLink Step-Into + +# End-to-end "F11 into library source" verification, one step beyond sourcelink.yaml. +# +# sourcelink.yaml (PR #208) proves every document in the PDB resolves to real +# GitHub content via `dotnet sourcelink test`. This workflow proves the *debugger* +# half: it builds a scratch consumer against the library (compiled with the same +# ContinuousIntegrationBuild + SourceLink a release ships), then drives netcoredbg +# to set a breakpoint in the consumer and STEP INTO (the F11 a consumer would +# press) a library constructor — asserting the debugger lands in the library's +# real source (symbols loaded, SourceLink-mapped source file) rather than a +# decompiled placeholder. If the SourceLink map or sequence points were broken the +# step would land with no source and this fails. +# +# Why a project reference, not the packed NuGet: netcoredbg (the only +# CI-scriptable .NET debugger) does not reliably pair a *package*-sourced +# assembly with its symbol-package PDB, so it cannot step into it. The PDB under +# test is byte-identical either way; the packed-package symbol/URL resolution is +# covered by sourcelink.yaml (PR #208). See .github/sourcelink/README.md. +# +# Together these two workflows satisfy #133 (SourceLink debug-step-into +# verification). Scheduled + manual only (not a PR gate): debugger automation is +# heavier and slightly less deterministic than a unit test, and SourceLink's raw +# URLs resolve only against a pushed commit. + +on: + schedule: + - cron: '30 7 * * 1' # Mondays 07:30 UTC + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: sourcelink-stepinto-${{ github.ref }} + cancel-in-progress: true + +env: + NETCOREDBG_VERSION: 3.1.2-1054 + # Reduce tiered-JIT reordering so the step-into target stays stable. + DOTNET_TieredCompilation: '0' + +jobs: + step-into: + name: Debugger step-into (F11) resolves library source + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + with: + persist-credentials: false + + - name: Detect src project + id: detect + run: | + if git ls-files 'src/**/*.csproj' 'src/*.csproj' | grep -q .; then + echo "found=true" >> "$GITHUB_OUTPUT" + else + echo "No src/**/*.csproj found — skipping." + echo "found=false" >> "$GITHUB_OUTPUT" + fi + + - name: Setup .NET + if: steps.detect.outputs.found == 'true' + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 + with: + dotnet-version: '10.0.x' + + # Build the fixture consumer. Its ProjectReference pulls in the library, + # compiled with ContinuousIntegrationBuild=true so the PDB carries the same + # SourceLink map a released package ships (Debug = non-optimized so the + # step-into target isn't reordered away). + - name: Build step-into consumer (SourceLink) + if: steps.detect.outputs.found == 'true' + run: | + dotnet build .github/sourcelink/consumer/StepIntoConsumer.csproj \ + -c Debug \ + -p:ContinuousIntegrationBuild=true + + - name: Install netcoredbg + if: steps.detect.outputs.found == 'true' + run: | + url="https://github.com/Samsung/netcoredbg/releases/download/${NETCOREDBG_VERSION}/netcoredbg-linux-amd64.tar.gz" + curl -sSfL "$url" -o netcoredbg.tar.gz + tar -xzf netcoredbg.tar.gz + echo "NETCOREDBG=$PWD/netcoredbg/netcoredbg" >> "$GITHUB_ENV" + + - name: Step into the library (F11) and assert real source resolves + if: steps.detect.outputs.found == 'true' + run: | + prog=".github/sourcelink/consumer/Program.cs" + line="$(grep -n 'STEP_INTO_TARGET' "$prog" | head -n1 | cut -d: -f1)" + echo "Breaking at Program.cs:$line" + python3 .github/sourcelink/verify_stepinto.py \ + "$NETCOREDBG" \ + ".github/sourcelink/consumer/bin/Debug/net10.0/StepIntoConsumer.dll" \ + "Program.cs:$line" \ + "TestExtractor.cs" From d9ca672c00c06e9b721f10aaa79080a5cf7a7ae8 Mon Sep 17 00:00:00 2001 From: Chris Wolfgang <210299580+Chris-Wolfgang@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:20:28 -0400 Subject: [PATCH 2/2] ci: align checkout SHA pin to repo canonical (9c091bb v7) Co-Authored-By: Claude Opus 4.8 --- .github/workflows/sourcelink-stepinto.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/sourcelink-stepinto.yaml b/.github/workflows/sourcelink-stepinto.yaml index 45b8d31..593867d 100644 --- a/.github/workflows/sourcelink-stepinto.yaml +++ b/.github/workflows/sourcelink-stepinto.yaml @@ -47,7 +47,7 @@ jobs: timeout-minutes: 20 steps: - name: Checkout code - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 with: persist-credentials: false