Skip to content

oracle: re-run on NLog to confirm the dispose-helper fix clears 4 tim… #40

oracle: re-run on NLog to confirm the dispose-helper fix clears 4 tim…

oracle: re-run on NLog to confirm the dispose-helper fix clears 4 tim… #40

Workflow file for this run

name: oracle (cross-tool)
# On-demand cross-tool validation: run Own.NET's leak check, Infer#, and CodeQL
# over the SAME public C# repo and diff their leak-class findings into an
# agreement report (scripts/oracle_compare.py). Evaluation tooling — the mature
# detectors are both a recall bar and an oracle. See docs/notes/oracle.md.
#
# Trigger from the Actions tab ("Run workflow") or the API. Inputs reach the
# shell via env (never interpolated into a `run:` script) to avoid injection.
#
# Honest notes:
# * Own.NET needs no build (error-tolerant SemanticModel). The two oracles do:
# CodeQL builds a database (build-mode: none, from source); Infer# analyses
# compiled .dll+.pdb, so the target must `dotnet build`. Each oracle step is
# continue-on-error, so a build failure still yields a partial report.
# * The diff core (oracle_compare.py) is unit-tested (--selftest, run first);
# this orchestration is validated on dispatch, like mine.yml.
on:
workflow_dispatch:
inputs:
repo:
description: "Target: owner/repo (e.g. DapperLib/Dapper) or a git URL"
required: true
ref:
description: "Branch / tag / sha to analyse (optional, default: repo HEAD)"
required: false
default: ""
paths:
description: "Subdir to scan with own-check (optional, default: whole repo)"
required: false
default: ""
build:
description: "Project/solution under the target to `dotnet build` for Infer# (optional, default: repo root)"
required: false
default: ""
include_tests:
description: "Also analyse test/benchmark/sample code (default: product code only)"
required: false
type: boolean
default: false
# Dev-loop fallback (the automation token can't `workflow_dispatch`): bump the
# sentinel corpus/oracle-target.txt to run the oracle push-triggered, reading the
# target from that file. Dev-branch only — remove before merging to main.
push:
branches:
- claude/zen-pasteur-76hfs1
- claude/mos-ownership-summary-n3q3j4
paths:
- corpus/oracle-target.txt
permissions:
contents: read
security-events: write # required by the CodeQL action internals (upload is off)
jobs:
oracle:
name: oracle ${{ inputs.repo }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.13"
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
# Resolve the target: workflow_dispatch inputs win; on push, read the
# sentinel corpus/oracle-target.txt (same format/allowlist as mine-on-push).
# Everything downstream reads steps.t.outputs.* so both triggers share steps.
- name: Resolve target
id: t
env:
IN_REPO: ${{ inputs.repo }}
IN_REF: ${{ inputs.ref }}
IN_PATHS: ${{ inputs.paths }}
IN_BUILD: ${{ inputs.build }}
IN_TESTS: ${{ inputs.include_tests }}
run: |
file=corpus/oracle-target.txt
if [[ -n "$IN_REPO" ]]; then
repo="$IN_REPO"; ref="$IN_REF"; paths="$IN_PATHS"; build="$IN_BUILD"; tests="$IN_TESTS"
else
repo=$(grep -vE '^[[:space:]]*(#|$)' "$file" | head -1 | tr -d '[:space:]')
ref=$(grep -E '^ref=' "$file" | head -1 | sed 's/^ref=//' | tr -d '[:space:]')
paths=$(grep -E '^paths=' "$file" | head -1 | sed 's/^paths=//' | tr -d '[:space:]')
build=$(grep -E '^build=' "$file" | head -1 | sed 's/^build=//' | tr -d '[:space:]')
tests=$(grep -E '^include_tests=' "$file"| head -1 | sed 's/^include_tests=//' | tr -d '[:space:]')
fi
# Allowlist: GitHub owner/repo, an https URL, or local:<in-repo path>
# (a fixture copied into target/ instead of cloned) — never an odd scheme.
if ! [[ "$repo" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ || "$repo" =~ ^https://[A-Za-z0-9./_-]+$ || "$repo" =~ ^local:[A-Za-z0-9._/-]+$ ]]; then
echo "oracle: invalid target '$repo'" >&2; exit 2
fi
{ echo "repo=$repo"; echo "ref=$ref"; echo "paths=$paths";
echo "build=$build"; echo "tests=${tests:-false}"; } >> "$GITHUB_OUTPUT"
echo "oracle target: $repo (ref='${ref:-HEAD}' paths='${paths:-*}' build='${build:-auto}' tests='${tests:-false}')"
# Fast fail: the diff logic is unit-tested before we clone/build anything.
- name: Comparator selftest
run: python scripts/oracle_compare.py --selftest
- name: Clone the target
env:
REPO: ${{ steps.t.outputs.repo }}
REF: ${{ steps.t.outputs.ref }}
run: |
if [[ "$REPO" == local:* ]]; then
# local:<in-repo path> — copy an in-repo fixture into target/ instead of
# cloning, so a tiny buildable repro (where ScreenToGif can't build on
# Linux) lets every oracle — including Infer#, which needs binaries — run.
src="${REPO#local:}"
[[ "$src" != *..* && -d "$src" ]] || { echo "oracle: bad local fixture '$src'" >&2; exit 2; }
cp -r "$src" target
echo "COMMIT=local@$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
echo "using in-repo fixture: $src"
else
case "$REPO" in
http://*|https://*|git@*) url="$REPO" ;;
*) url="https://github.com/${REPO}.git" ;;
esac
if [[ -n "$REF" ]]; then
# A blobless clone keeps the full commit/ref graph (blobs fetched on
# demand), so ANY ref — branch, tag, or an abbreviated SHA — resolves
# locally at checkout. A shallow `fetch origin <short-sha>` does NOT:
# servers reject abbreviated / unadvertised SHAs in a want request.
git clone --filter=blob:none --quiet "$url" target
git -C target checkout --quiet --detach "$REF"
else
git clone --depth 1 --quiet "$url" target
fi
echo "COMMIT=$(git -C target rev-parse HEAD)" >> "$GITHUB_ENV"
fi
# Framework-type references for own-check. The Roslyn extractor resolves a
# `+=` only when the event's declaring type is on its reference set — else the
# subscription is an OWN050 "unchecked" note, not a leak. Materialize the
# WindowsDesktop ref pack (WPF / WinForms / Microsoft.Win32.SystemEvents) and
# export OWN_EXTRA_REF_DIRS so own-check resolves framework events instead of
# dropping them — putting it on equal footing with CodeQL, which resolves
# types from source. Harmless for non-Windows targets (deduped vs the runtime).
- name: Materialize framework reference assemblies
continue-on-error: true
run: |
tmp=$(mktemp -d)
printf '%s\n' \
'<Project Sdk="Microsoft.NET.Sdk">' \
' <PropertyGroup>' \
' <TargetFramework>net8.0-windows</TargetFramework>' \
' <UseWPF>true</UseWPF>' \
' <UseWindowsForms>true</UseWindowsForms>' \
' <EnableWindowsTargeting>true</EnableWindowsTargeting>' \
' </PropertyGroup>' \
'</Project>' > "$tmp/ref.csproj"
dotnet restore "$tmp/ref.csproj" >/dev/null 2>&1 || echo "ref restore failed (continuing)"
d=$(find "$HOME/.nuget/packages/microsoft.windowsdesktop.app.ref" -type d -name 'net8.0' 2>/dev/null | sort | tail -1 || true)
if [ -n "$d" ]; then
echo "OWN_EXTRA_REF_DIRS=$d" >> "$GITHUB_ENV"
echo "framework refs: $d ($(find "$d" -name '*.dll' | wc -l) dlls)"
else
echo "framework refs not found — own-check resolves runtime types only"
fi
# Own.NET — no build needed; scans .cs directly. --format sarif so the diff
# reads our findings through the SAME parser as the Infer#/CodeQL SARIF (no
# bespoke text parser, no parser drift; own.txt holds a SARIF log — the
# comparator sniffs the format). --severity warning so the warning-tier leaks
# (injected-source subscriptions, e.g. VideoSource) are included, not just the
# provable static-source errors. OWN_EXTRA_REF_DIRS (above) is inherited by the
# extractor process.
- name: Own.NET own-check
env:
PATHS: ${{ steps.t.outputs.paths }}
run: |
scan="target"; [[ -n "$PATHS" ]] && scan="target/$PATHS"
set +e
scripts/own-check.sh --format sarif --severity warning -- "$scan" > own.txt 2> own-extract.log
echo "own-check rc=$? ; own.txt is a SARIF log ($(wc -c < own.txt) bytes)"
# CodeQL — database from source (no build). The dispose/leak queries
# (cs/local-not-disposed & friends) are *quality* queries, absent from the
# default code-scanning (security) suite — so request security-and-quality,
# else CodeQL silently contributes zero. Comparator filters to the leak family.
- name: CodeQL init
uses: github/codeql-action/init@v4
continue-on-error: true
with:
languages: csharp
build-mode: none
source-root: target
queries: security-and-quality
- name: CodeQL analyze
uses: github/codeql-action/analyze@v4
continue-on-error: true
with:
category: ownnet-oracle
output: codeql-out
upload: false
# Infer# — needs compiled binaries; build the target into one output dir.
# Choose what to build: explicit `build` input wins; else the product
# library (`<repo>.csproj` outside tests/benchmarks — a leak scan wants the
# library, and building the whole solution often drags in unbuildable test
# projects); else a lone solution (.sln/.slnx); else the dir (-> partial).
- name: Build the target (for Infer#)
env:
BUILD: ${{ steps.t.outputs.build }}
REPO: ${{ steps.t.outputs.repo }}
continue-on-error: true
run: |
if [[ -n "$BUILD" ]]; then
tgt="target/$BUILD"
else
# The main library is almost always named after the repo; prefer a
# unique <repo>.csproj outside the test/benchmark/sample/example trees
# (-ipath: case-insensitive, so Tests/ Benchmarks/ etc. are excluded too).
name="${REPO##*/}"; name="${name%.git}"
mapfile -t named < <(find target -type f -name "$name.csproj" \
-not -ipath '*/test/*' -not -ipath '*/tests/*' -not -ipath '*/benchmark*' \
-not -ipath '*/sample*' -not -ipath '*/example*' | sort)
mapfile -t root_slns < <(find target -maxdepth 1 \( -name '*.sln' -o -name '*.slnx' \) | sort)
mapfile -t all_slns < <(find target \( -name '*.sln' -o -name '*.slnx' \) | sort)
if [[ ${#named[@]} -eq 1 ]]; then
tgt="${named[0]}"
elif [[ ${#root_slns[@]} -eq 1 ]]; then
tgt="${root_slns[0]}"
elif [[ ${#root_slns[@]} -eq 0 && ${#all_slns[@]} -eq 1 ]]; then
tgt="${all_slns[0]}"
else
tgt="target"
echo "note: no unique '$name.csproj' and ${#root_slns[@]} root / ${#all_slns[@]} total solution(s); pass the 'build' input to disambiguate"
fi
fi
# Some repos (e.g. those using Nerdbank.GitVersioning) need git history to
# compute a version at build time; the clone is shallow, so deepen it.
# Harmless (|| true) when the repo is already complete or doesn't need it.
git -C target fetch --unshallow --tags --quiet 2>/dev/null || true
echo "Infer# build target: $tgt"
if dotnet build "$tgt" -c Release -o _bin -v quiet; then
echo "BUILD_OK=1" >> "$GITHUB_ENV"
else
echo "target build failed — Infer# will be skipped, report stays partial"
fi
- name: Run Infer#
if: env.BUILD_OK == '1'
uses: microsoft/infersharpaction@v1.5
continue-on-error: true
with:
binary-path: _bin
- name: Diff Own.NET vs the oracles
if: always()
env:
REPO: ${{ steps.t.outputs.repo }}
INCLUDE_TESTS: ${{ steps.t.outputs.tests }}
run: |
args=(--own own.txt --target "$REPO" --commit "${COMMIT:-}"
--strip "$GITHUB_WORKSPACE/target" --strip target
--json report.json)
# Compare on product code by default: tests/benchmarks pollute the diff
# (and Infer# only built the product project). include_tests keeps them.
[[ "${INCLUDE_TESTS,,}" == "true" ]] || args+=(--exclude-tests)
# Suppress verified false positives (triaged against real source) so the
# triage queue surfaces only NEW own-only findings. Matched by name, not
# line, so it survives the target drifting at HEAD. See the file header and
# docs/notes/oracle-known-fps.md.
[[ -f corpus/oracle-fp-baseline.txt ]] && args+=(--baseline corpus/oracle-fp-baseline.txt)
[[ -f infer-out/report.sarif ]] && args+=(--infersharp infer-out/report.sarif)
cq=$(find codeql-out -name '*.sarif' -type f 2>/dev/null | head -1 || true)
[[ -n "$cq" ]] && args+=(--codeql "$cq")
python scripts/oracle_compare.py "${args[@]}" > report.md
cat report.md
- name: Publish the report to the run summary
if: always()
run: |
if [[ -s report.md ]]; then
cat report.md >> "$GITHUB_STEP_SUMMARY"
else
echo "no report produced (see the Diff step log)" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload the report and raw outputs
if: always()
uses: actions/upload-artifact@v4
with:
name: oracle-report
path: |
report.md
report.json
own.txt
own-extract.log
infer-out/report.sarif
codeql-out/*.sarif
if-no-files-found: warn