diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8de6e2a9..d26ee060 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2112,8 +2112,10 @@ jobs: # The PowerShell wrapper's exit-code tiers, on the platform it exists for # (#313). own-check.ps1 is the Windows twin of own-check.sh and is never - # exercised by any other job: the composite action drives the .sh, and the - # .ps1's paths are built with backslashes, so it cannot run on Linux at all. + # exercised by any other job. The composite action drives own-check.sh. The + # PowerShell wrapper can execute far enough on Linux to prove its stage-1 + # failure tier, but its successful extraction path constructs Windows-style + # paths and is therefore exercised end to end on Windows. # That left the stage-1 normalisation — a broken extractor must land in the # >=2 tier, not borrow exit 1 from "findings present" — mirrored in code and # proven nowhere. Symmetry of source is an argument, not evidence. @@ -2157,6 +2159,21 @@ jobs: New-Item -ItemType Directory -Force -Path $clean | Out-Null 'public class Clean { public int M() { return 1; } }' | Set-Content -Path (Join-Path $clean "Clean.cs") + - name: Fixtures for the invocation contract (a leaky target, a clean cwd) + run: | + # The target the user will ASK for, and a working directory that has + # nothing to report — so "scanned the wrong tree" cannot hide behind + # a finding that happens to exist in both. + foreach ($n in @("ps1-target-a", "ps1-target-b")) { + $d = Join-Path $env:RUNNER_TEMP $n + New-Item -ItemType Directory -Force -Path $d | Out-Null + "using System.IO;`npublic class Leaky_$($n -replace '-','_') { public void Run() { var s = new MemoryStream(); s.WriteByte(1); } }" | + Set-Content -Path (Join-Path $d "Leaky.cs") + } + $cwd = Join-Path $env:RUNNER_TEMP "ps1-cwd-clean" + New-Item -ItemType Directory -Force -Path $cwd | Out-Null + 'public class NothingHere { public int M() { return 1; } }' | + Set-Content -Path (Join-Path $cwd "NothingHere.cs") - name: "Tier >=2 — a broken stage 1 is a hard error, never the findings tier" run: | # -Root at a checkout that has no extractor project: `dotnet run` @@ -2201,6 +2218,51 @@ jobs: } Write-Host "OK: findings -> 1 with the flag, 0 without, identical output" exit 0 + - name: "Invocation contract — the requested target is what gets scanned (#315)" + run: | + # The defect this pins: `own-check.ps1 -Format github -- src\App` used + # to bind src\App to -Root and scan "." instead — the tool looked, but + # not where it was asked to. Standing in a CLEAN directory is what + # makes that visible: a wrong-tree scan comes back with nothing and + # reads as good news. + $script = Join-Path $env:GITHUB_WORKSPACE "scripts/own-check.ps1" + $targetA = Join-Path $env:RUNNER_TEMP "ps1-target-a" + $targetB = Join-Path $env:RUNNER_TEMP "ps1-target-b" + Push-Location (Join-Path $env:RUNNER_TEMP "ps1-cwd-clean") + try { + $sep = & $script -Format github -- $targetA + $expl = & $script -Format github -Paths $targetA + $pos = & $script -Format github $targetA + $many = & $script -Format github -- $targetA $targetB + $none = & $script -Format github + } finally { Pop-Location } + + foreach ($case in @(@{n="-- "; o=$sep}, @{n="-Paths "; o=$expl}, @{n="bare positional"; o=$pos})) { + if (-not (($case.o -join "`n") -match "OWN001")) { + Write-Host "FAIL: $($case.n) reported no finding — the target was not scanned"; exit 1 + } + if (($case.o -join "`n") -match "NothingHere") { + Write-Host "FAIL: $($case.n) scanned the working directory instead of the target"; exit 1 + } + } + # The two public forms must be EQUIVALENT, not merely both non-empty. + if (($sep -join "`n").Trim() -ne ($expl -join "`n").Trim()) { + Write-Host "FAIL: '-- ' and '-Paths ' disagree" + Write-Host "--- -- form ---"; $sep | Write-Host + Write-Host "--- -Paths form ---"; $expl | Write-Host + exit 1 + } + # [string[]] is the declared type, so more than one path must work. + $manyText = $many -join "`n" + if (-not ($manyText -match "ps1-target-a") -or -not ($manyText -match "ps1-target-b")) { + Write-Host "FAIL: multiple positional paths did not both reach -Paths"; $many | Write-Host; exit 1 + } + # No target at all still means the working directory, which is clean. + if (($none -join "`n") -match "OWN001") { + Write-Host "FAIL: with no target the clean cwd should report nothing"; $none | Write-Host; exit 1 + } + Write-Host "OK: every documented form scans the requested target; the two public forms agree" + exit 0 # Dog-food the code-scanning surface end-to-end (P-013): run the composite action # with format: sarif over the sample tree, then upload the log to GitHub code diff --git a/scripts/own-check.ps1 b/scripts/own-check.ps1 index 8ecd0996..9140a611 100644 --- a/scripts/own-check.ps1 +++ b/scripts/own-check.ps1 @@ -44,7 +44,7 @@ .EXAMPLE scripts\own-check.ps1 -Format github -Severity warning -FailOnFinding -- . #> -[CmdletBinding()] +[CmdletBinding(PositionalBinding = $false)] param( [string]$Root, [string]$Format = "human", @@ -53,7 +53,13 @@ param( [string]$Verbosity = "normal", [switch]$Legacy, [switch]$FailOnFinding, - [Parameter(ValueFromRemainingArguments = $true)] + # Position 0 is claimed EXPLICITLY, and automatic positional binding is off + # for everything else (PositionalBinding = $false). Without both halves the + # scan target lands in $Root — the first declared parameter took position 0 + # — and own-check then hunts for the extractor inside the tree it was asked + # to scan, or silently scans "." instead. Declaring the contract beats + # relying on declaration order to keep meaning it. + [Parameter(Position = 0, ValueFromRemainingArguments = $true)] [string[]]$Paths ) @@ -64,7 +70,10 @@ $ErrorActionPreference = "Stop" if ([string]::IsNullOrEmpty($Root)) { $Root = Split-Path -Parent $PSScriptRoot } -# A bare "--" separator (shell habit) is harmless; drop it. +# A bare "--" separator (shell habit) is harmless; drop it. In an interactive +# session PowerShell eats the token itself, so this is a no-op there — it earns +# its keep when the arguments are splatted (`& own-check.ps1 @args`), where a +# literal "--" does arrive as a value. if ($Paths) { $Paths = @($Paths | Where-Object { $_ -ne "--" }) } if (-not $Paths -or $Paths.Count -eq 0) { $Paths = @(".") }