Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/license-audit/allowed-licenses.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[
"MIT",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"MS-PL",
"0BSD",
"ISC",
"BSL-1.0",
"MPL-2.0"
]
3 changes: 3 additions & 0 deletions .github/license-audit/ignored-packages.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[
"SonarAnalyzer.CSharp"
]
5 changes: 5 additions & 0 deletions .github/license-audit/url-license-mappings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"http://go.microsoft.com/fwlink/?LinkId=329770": "MIT",
"https://github.com/dotnet/standard/blob/master/LICENSE.TXT": "MIT",
"https://raw.githubusercontent.com/xunit/xunit/master/license.txt": "Apache-2.0"
}
9 changes: 9 additions & 0 deletions .github/sourcelink/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!--
Isolation stub. MSBuild imports only the NEAREST Directory.Build.props when
walking up, so this empty project stops the repo-root Directory.Build.props
(analyzers, BannedSymbols, TreatWarningsAsErrors, multi-TFM, PublicAPI, …)
from applying to the throwaway SourceLink step-into consumer. The consumer is
a debugging fixture, not shipped code, and must not inherit library policy.
-->
<Project>
</Project>
3 changes: 3 additions & 0 deletions .github/sourcelink/Directory.Build.targets
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<!-- Isolation stub — see Directory.Build.props in this folder. -->
<Project>
</Project>
51 changes: 51 additions & 0 deletions .github/sourcelink/README.md
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 14 additions & 0 deletions .github/sourcelink/consumer/Program.cs
Original file line number Diff line number Diff line change
@@ -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<int> items = new[] { 1, 2, 3 };
var extractor = new TestExtractor<int>(items); // STEP_INTO_TARGET
Console.WriteLine(extractor.GetType().FullName);
29 changes: 29 additions & 0 deletions .github/sourcelink/consumer/StepIntoConsumer.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<Project Sdk="Microsoft.NET.Sdk">

<!--
A minimal consumer that steps into the library. It references the library
PROJECT (built with ContinuousIntegrationBuild=true so the PDB carries the
same SourceLink map a released package ships). A ProjectReference is used
rather than a PackageReference because netcoredbg — the only CI-scriptable
.NET debugger — does not reliably pair a package-sourced assembly with its
symbol package's PDB, so it cannot step into it. The SourceLink PDB under
test is identical either way; the packed-package symbol/URL resolution is
covered by sourcelink.yaml (#133 / PR #208).
-->
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>disable</ImplicitUsings>
<!-- Non-optimized so the step-into target is not reordered/inlined away. -->
<Optimize>false</Optimize>
<DebugType>portable</DebugType>
<!-- Set by the workflow to the repo's src project path. -->
<TestKitProject Condition="'$(TestKitProject)' == ''">../../../src/Wolfgang.Etl.TestKit/Wolfgang.Etl.TestKit.csproj</TestKitProject>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="$(TestKitProject)" />
</ItemGroup>

</Project>
115 changes: 115 additions & 0 deletions .github/sourcelink/verify_stepinto.py
Original file line number Diff line number Diff line change
@@ -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 <netcoredbg> <consumer.dll> <Program.cs:LINE> <expected-src.cs>
"""
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)
89 changes: 89 additions & 0 deletions .github/workflows/actions-audit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
name: Actions Audit

# GitHub Actions workflow security/quality audit (#143).
#
# - actionlint: static checker for workflow YAML + embedded shell (via
# shellcheck). Gates the build on findings — workflows should be lint-clean.
# - zizmor: security auditor for Actions (injection, unpinned actions,
# over-broad permissions, ...). Reports to the Security tab (SARIF) rather
# than hard-gating, since it is opinionated; promote to a gate once the
# baseline is clean.
#
# Installed via `go install` / `pip install` (pinned) rather than third-party
# wrapper actions, to keep the supply chain small.

on:
# Runs on every PR (not path-filtered) so the actionlint job can be a required
# status check — a path-filtered required check would hang PRs that don't touch
# .github/workflows/**. actionlint/zizmor are cheap (~20s) and idempotent.
pull_request:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: actions-audit-${{ github.ref }}
cancel-in-progress: true

jobs:
actionlint:
name: actionlint
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Setup Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version: 'stable'

- name: Install actionlint
run: go install github.com/rhysd/actionlint/cmd/actionlint@v1.7.7

- name: Run actionlint
env:
# Gate on shellcheck warning+ only. The info/style nits (SC2012 "use
# find not ls", SC2035 "use ./*glob*") in the canonical pr.yaml are
# not worth failing every PR over; warnings and errors still gate.
SHELLCHECK_OPTS: --severity=warning
run: actionlint -color

zizmor:
name: zizmor
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
security-events: write
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Setup Python
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.x'

- name: Install zizmor
run: pip install zizmor

- name: Run zizmor
env:
GH_TOKEN: ${{ github.token }}
# Report-only for now: don't fail the job on findings, upload them to
# Code Scanning instead. Remove `|| true` to turn this into a gate.
run: zizmor --config .zizmor.yml --format sarif .github/workflows/ > zizmor.sarif || true

- name: Upload zizmor SARIF
uses: github/codeql-action/upload-sarif@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4
with:
sarif_file: zizmor.sarif
53 changes: 53 additions & 0 deletions .github/workflows/aot-smoke.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: AOT Smoke

# Native-AOT publish-and-run smoke consumer (#132).
#
# Publishes a tiny console consumer with PublishAot and runs the native binary,
# so the build fails if the library stops compiling or running under native AOT
# (e.g. the async-enumerable pipeline path stops being trimmer-safe). This is the
# *runtime* half of AOT verification: it exercises TestExtractor / TestTransformer
# / TestLoader end-to-end in a trimmed, natively-compiled binary.
#
# windows-latest ships the MSVC "Desktop development with C++" workload that the
# native linker requires, so no extra toolchain install is needed.
#
# Runs on every PR so AOT regressions block before merge.

on:
pull_request:
push:
branches: [main]
workflow_dispatch:

permissions:
contents: read

concurrency:
group: aot-smoke-${{ github.ref }}
cancel-in-progress: true

jobs:
aot-smoke:
name: "AOT publish + run"
runs-on: windows-latest
timeout-minutes: 20
steps:
- name: Checkout code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6
with:
dotnet-version: '10.0.x'

- name: Publish (native AOT, win-x64)
run: dotnet publish aot-smoke/Wolfgang.Etl.TestKit.AotSmoke/Wolfgang.Etl.TestKit.AotSmoke.csproj -c Release -r win-x64

- name: Run native binary
shell: bash
run: |
EXE="aot-smoke/Wolfgang.Etl.TestKit.AotSmoke/bin/Release/net10.0/win-x64/publish/Wolfgang.Etl.TestKit.AotSmoke.exe"
echo "Running: $EXE"
"$EXE"
Loading
Loading