oracle: re-run on NLog to confirm the dispose-helper fix clears 4 tim… #40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |