ledger: discovered pr#8841 actor=claude-wpf-bot #962
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
| # .github/workflows/build.yml | |
| name: Build | |
| on: | |
| pull_request: | |
| branches: | |
| - if/main | |
| - if/staging | |
| paths-ignore: | |
| - "docs/**" | |
| - "*.md" | |
| push: | |
| branches: | |
| - if/main | |
| - if/staging | |
| workflow_dispatch: | |
| concurrency: | |
| group: build-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| checks: write | |
| id-token: write | |
| packages: write | |
| env: | |
| IF_REPO_ROOT: ${{ github.workspace }} | |
| jobs: | |
| # --------------------------------------------------------------------------- | |
| # lint-tools: Python tooling quality gate (ubuntu runner) | |
| # --------------------------------------------------------------------------- | |
| lint-tools: | |
| name: Lint Python tools | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install lint tools | |
| # jsonschema is required by tools/check-config-schema.py for full | |
| # draft 2020-12 validation (without it the validator falls back to a | |
| # type-only stdlib check that misses enum constraints, so e.g. | |
| # schema_version=99 would be incorrectly accepted). | |
| run: pip install ruff mypy pytest pyyaml types-PyYAML jsonschema | |
| - name: Set up ephemeral GPG signing key for ledger tests | |
| # tools/ledger-event.py fails closed in CI environments when git | |
| # commit signing fails (HIGH-2 fix). Tests that exercise the real | |
| # ledger-event subprocess therefore need a GPG key on the runner. | |
| # Generate an ephemeral throw-away key valid for one day; it never | |
| # leaves the runner and is overwritten on every CI run. | |
| run: | | |
| set -euo pipefail | |
| cat > /tmp/gpg-batch <<'EOF' | |
| %no-protection | |
| Key-Type: RSA | |
| Key-Length: 2048 | |
| Name-Real: CI Test | |
| Name-Email: ci-test@example.com | |
| Expire-Date: 1 | |
| %commit | |
| EOF | |
| gpg --batch --gen-key /tmp/gpg-batch | |
| rm /tmp/gpg-batch | |
| KEY_ID=$(gpg --list-secret-keys --keyid-format=long ci-test@example.com | awk '/^sec/ {split($2,a,"/"); print a[2]; exit}') | |
| echo "GPG key id: $KEY_ID" | |
| git config --global user.name "CI Test" | |
| git config --global user.email "ci-test@example.com" | |
| git config --global user.signingkey "$KEY_ID" | |
| git config --global commit.gpgsign true | |
| - name: ruff check | |
| run: ruff check tools/ tests/ | |
| # mypy --strict applies to tools/ only (per pyproject.toml | |
| # [tool.mypy] files=["tools/*.py"]). Test code is intentionally not | |
| # type-strict — tests use yaml.safe_load() returns and parametrize | |
| # with untyped values where strict typing would add noise without | |
| # catching real bugs. | |
| - name: mypy --strict (tools) | |
| run: mypy --strict tools/ | |
| - name: pytest tools unit tests | |
| run: pytest tests/ -v --tb=short | |
| # --------------------------------------------------------------------------- | |
| # lint-yaml: YAML / actionlint gate (ubuntu runner) | |
| # --------------------------------------------------------------------------- | |
| lint-yaml: | |
| name: Lint YAML / actionlint | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python (for PyYAML parse check) | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install PyYAML | |
| run: pip install pyyaml | |
| - name: Validate all workflow YAML parses | |
| run: | | |
| python - <<'EOF' | |
| import sys, glob, yaml | |
| errors = [] | |
| for path in glob.glob(".github/workflows/*.yml"): | |
| try: | |
| with open(path) as f: | |
| yaml.safe_load(f) | |
| print(f"OK: {path}") | |
| except yaml.YAMLError as e: | |
| errors.append(f"FAIL: {path}: {e}") | |
| if errors: | |
| for err in errors: | |
| print(err, file=sys.stderr) | |
| sys.exit(1) | |
| EOF | |
| - name: actionlint | |
| continue-on-error: true | |
| run: | | |
| if command -v actionlint &>/dev/null; then | |
| actionlint .github/workflows/*.yml | |
| else | |
| echo "actionlint not installed; skipping (install from https://github.com/rhysd/actionlint)" | |
| fi | |
| # --------------------------------------------------------------------------- | |
| # build-wpf: Windows matrix build (x64 + arm64) | |
| # --------------------------------------------------------------------------- | |
| build-wpf: | |
| name: Build WPF (${{ matrix.arch }}) | |
| runs-on: windows-2022 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: [x64, arm64] | |
| # arm64 cross-compile runs on the windows-2022 x64 runner (VS2022 / v143 | |
| # platform toolset, which the native WpfGfx vcxproj files require). | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Cache NuGet packages | |
| uses: actions/cache@v4 | |
| with: | |
| path: ~/.nuget/packages | |
| key: nuget-win-${{ matrix.arch }}-${{ hashFiles('**/packages.lock.json', 'global.json') }} | |
| restore-keys: nuget-win-${{ matrix.arch }}- | |
| - name: Set up .NET SDK | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| global-json-file: global.json | |
| - name: Install Strawberry Perl (idempotent) | |
| # windows-2022 pre-installs strawberryperl (currently v5.42+). | |
| # Skip choco install if perl is already on PATH; otherwise install. | |
| run: | | |
| if (Get-Command perl -ErrorAction SilentlyContinue) { | |
| Write-Host "Strawberry Perl already installed: $(perl --version | Select-String -Pattern 'This is perl')" | |
| } else { | |
| choco install strawberryperl -y --no-progress | |
| } | |
| shell: powershell | |
| - name: Validate ledger schema | |
| run: python tools/ledger-validate.py --ledger .if-fork/patch-ledger.jsonl | |
| shell: bash | |
| # ----- Real upstream WPF build ----- | |
| # The fork's checkout already contains the full dotnet/wpf source | |
| # tree (since this repo IS a fork of dotnet/wpf). Run the standard | |
| # arcade build to produce the four managed assemblies that | |
| # InitialForce.WPF wraps. | |
| - name: Build upstream WPF source | |
| # build.cmd already passes -restore -build to Build.ps1; do not duplicate. | |
| run: | | |
| .\build.cmd -c Release -plat ${{ matrix.arch }} -pack /bl:artifacts/log/Release/build.${{ matrix.arch }}.binlog | |
| shell: cmd | |
| timeout-minutes: 60 | |
| - name: Locate built WPF DLLs | |
| id: locate-dlls | |
| run: | | |
| $names = @("PresentationCore", "PresentationFramework", "WindowsBase", "System.Xaml") | |
| $found = @{} | |
| foreach ($n in $names) { | |
| # Canonical path: artifacts\bin\<exact-name>\<arch>\Release\<tfm>\<name>.dll | |
| # Restrict the search to the assembly's own bin tree so Tests/ref/obj | |
| # copies cannot be picked up by accident. | |
| $root = "artifacts\bin\$n\${{ matrix.arch }}\Release" | |
| if (-not (Test-Path $root)) { | |
| Write-Error "Output dir $root does not exist; upstream build did not produce $n" | |
| Get-ChildItem -Recurse artifacts\bin -Filter "$n.dll" | Format-Table FullName | |
| exit 1 | |
| } | |
| $hit = Get-ChildItem -Recurse $root -Filter "$n.dll" -ErrorAction SilentlyContinue | | |
| Where-Object { $_.FullName -notmatch "\\ref\\|\\obj\\" } | | |
| Select-Object -First 1 | |
| if (-not $hit) { | |
| Write-Error "Could not locate $n.dll under $root" | |
| Get-ChildItem -Recurse $root -Filter "*.dll" | Format-Table FullName | |
| exit 1 | |
| } | |
| Write-Host "Found $n.dll at $($hit.FullName)" | |
| $found[$n] = $hit.FullName | |
| } | |
| # Persist paths for the next step via env | |
| "PRESENTATIONCORE_DLL=$($found.PresentationCore)" | Out-File -Append $env:GITHUB_ENV | |
| "PRESENTATIONFRAMEWORK_DLL=$($found.PresentationFramework)" | Out-File -Append $env:GITHUB_ENV | |
| "WINDOWSBASE_DLL=$($found.WindowsBase)" | Out-File -Append $env:GITHUB_ENV | |
| "SYSTEMXAML_DLL=$($found.'System.Xaml')" | Out-File -Append $env:GITHUB_ENV | |
| shell: powershell | |
| - name: Stage DLLs into both packaging trees | |
| run: | | |
| $rid = if ("${{ matrix.arch }}" -eq "x64") { "win-x64" } else { "win-arm64" } | |
| $destinations = @( | |
| "packaging\InitialForce.WPF\runtimes\$rid\lib\net10.0", | |
| "packaging\InitialForce.WPF.RuntimeOverride\runtimes\$rid\lib\net10.0" | |
| ) | |
| foreach ($dest in $destinations) { | |
| New-Item -ItemType Directory -Path $dest -Force | Out-Null | |
| foreach ($pair in @( | |
| @{ name = "PresentationCore"; path = "$env:PRESENTATIONCORE_DLL" }, | |
| @{ name = "PresentationFramework"; path = "$env:PRESENTATIONFRAMEWORK_DLL" }, | |
| @{ name = "WindowsBase"; path = "$env:WINDOWSBASE_DLL" }, | |
| @{ name = "System.Xaml"; path = "$env:SYSTEMXAML_DLL" } | |
| )) { | |
| Copy-Item $pair.path -Destination $dest | |
| $pdb = $pair.path -replace '\.dll$', '.pdb' | |
| if (Test-Path $pdb) { Copy-Item $pdb -Destination $dest } | |
| } | |
| Write-Host "==> Staged in $dest" | |
| Get-ChildItem $dest | Format-Table Name, Length | |
| } | |
| shell: powershell | |
| # ----- crossgen2 (ReadyToRun) compilation ----- | |
| # Stock dotnet/wpf DLLs in Microsoft.WindowsDesktop.App ship with R2R native | |
| # code baked in by dotnet/runtime's runtime-pack assembly step. Our fork | |
| # builds the libraries via build.cmd but does not run that step, so the DLLs | |
| # in artifacts/bin are JIT-only. JIT'd frames are slightly fatter than R2R'd | |
| # frames, and WPF code paths that are already deep on the stack | |
| # (dispatcher unhandled-exception handler -> MessageDialog.xaml -> BAML -> | |
| # WPFLocalizeExtension's culture iteration) overflow the 1 MB thread stack | |
| # in consumers. R2R-compile the staged DLLs in place to match upstream behavior. | |
| - name: R2R-compile staged DLLs (crossgen2) | |
| run: | | |
| $rid = if ("${{ matrix.arch }}" -eq "x64") { "win-x64" } else { "win-arm64" } | |
| # Locate runtime packs from the SDK installed by setup-dotnet. | |
| $sdkInfo = (& dotnet --info) -join "`n" | |
| $dotnetRoot = $env:DOTNET_ROOT | |
| if (-not $dotnetRoot -or -not (Test-Path $dotnetRoot)) { | |
| $dotnetRoot = Split-Path -Parent (Get-Command dotnet).Source | |
| } | |
| $netCorePack = Get-ChildItem (Join-Path $dotnetRoot 'shared\Microsoft.NETCore.App') -Directory | | |
| Sort-Object Name -Descending | Select-Object -First 1 | |
| $desktopPack = Get-ChildItem (Join-Path $dotnetRoot 'shared\Microsoft.WindowsDesktop.App') -Directory | | |
| Sort-Object Name -Descending | Select-Object -First 1 | |
| if (-not $netCorePack) { throw "Microsoft.NETCore.App runtime pack not found under $dotnetRoot" } | |
| if (-not $desktopPack) { throw "Microsoft.WindowsDesktop.App runtime pack not found under $dotnetRoot" } | |
| Write-Host "Using NetCorePack = $($netCorePack.FullName)" | |
| Write-Host "Using DesktopPack = $($desktopPack.FullName)" | |
| $stagingDirs = @( | |
| "packaging\InitialForce.WPF\runtimes\$rid\lib\net10.0", | |
| "packaging\InitialForce.WPF.RuntimeOverride\runtimes\$rid\lib\net10.0" | |
| ) | |
| foreach ($dir in $stagingDirs) { | |
| Write-Host "" | |
| Write-Host "===================================================================" | |
| Write-Host "Crossgen2: $dir" | |
| Write-Host "===================================================================" | |
| & .\tools\crossgen-staged.ps1 ` | |
| -StagingDir $dir ` | |
| -NetCorePack $netCorePack.FullName ` | |
| -DesktopPack $desktopPack.FullName ` | |
| -TargetArch ${{ matrix.arch }} | |
| if ($LASTEXITCODE -ne 0) { throw "crossgen-staged.ps1 failed for $dir" } | |
| } | |
| shell: powershell | |
| - name: Compute package version | |
| id: version | |
| run: | | |
| # Pre-release version in the form 10.0.0-if.<run_number>+<short_sha> | |
| $sha = "${{ github.sha }}".Substring(0, 7) | |
| $version = "10.0.0-if.${{ github.run_number }}+$sha" | |
| "PKG_VERSION=$version" | Out-File -Append $env:GITHUB_ENV | |
| Write-Host "Package version: $version" | |
| shell: powershell | |
| - name: Pack InitialForce.WPF (only on x64 host) | |
| if: matrix.arch == 'x64' | |
| run: | | |
| dotnet pack packaging\InitialForce.WPF\InitialForce.WPF.csproj ` | |
| -c Release ` | |
| -p:PackageVersion=$env:PKG_VERSION ` | |
| --output artifacts\packages | |
| shell: powershell | |
| - name: Pack InitialForce.WPF.RuntimeOverride (only on x64 host) | |
| if: matrix.arch == 'x64' | |
| run: | | |
| dotnet pack packaging\InitialForce.WPF.RuntimeOverride\InitialForce.WPF.RuntimeOverride.csproj ` | |
| -c Release ` | |
| -p:PackageVersion=$env:PKG_VERSION ` | |
| --output artifacts\packages | |
| shell: powershell | |
| - name: List produced packages | |
| run: | | |
| Get-ChildItem -Recurse artifacts\packages | Format-Table FullName, Length | |
| shell: powershell | |
| # Publish the just-packed nupkgs to nuget.org (public). | |
| # Conditions: | |
| # - Only on push events (skip pull_request) so PRs don't churn the feed | |
| # - Only on if/main (skip if/staging) | |
| # - Only on x64 (matches the existing pack gate) | |
| # --skip-duplicate makes the step idempotent if a previous run already | |
| # pushed a package with the same exact version. | |
| - name: Publish to nuget.org | |
| if: matrix.arch == 'x64' && github.event_name == 'push' && github.ref == 'refs/heads/if/main' | |
| run: | | |
| $files = Get-ChildItem artifacts\packages -Filter '*.nupkg' | |
| foreach ($f in $files) { | |
| Write-Host "Pushing $($f.Name) to nuget.org..." | |
| dotnet nuget push $f.FullName ` | |
| --source "https://api.nuget.org/v3/index.json" ` | |
| --api-key "${{ secrets.NUGET_ORG_API_KEY }}" ` | |
| --skip-duplicate | |
| } | |
| shell: powershell | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: build-${{ matrix.arch }} | |
| path: | | |
| artifacts/packages/**/*.nupkg | |
| artifacts/packages/**/*.snupkg | |
| packaging/InitialForce.WPF/runtimes/** | |
| if-no-files-found: error | |
| retention-days: 14 | |
| - name: Upload binary logs on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: binlogs-${{ matrix.arch }} | |
| path: artifacts/log/**/*.binlog | |
| retention-days: 7 | |
| # --------------------------------------------------------------------------- | |
| # smoke: real smoke harness run against the packed nupkg. | |
| # | |
| # x64 only: the pack steps in build-wpf run under `if: matrix.arch == 'x64'` | |
| # (only build-x64 carries a nupkg), and the smoke project is pinned to the | |
| # win-x64 RID, so there is no arm64 package to consume. | |
| # --------------------------------------------------------------------------- | |
| smoke: | |
| name: Smoke (${{ matrix.arch }}) | |
| runs-on: windows-2022 | |
| needs: build-wpf | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: [x64] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Download build artifacts (packed nupkg) | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: build-${{ matrix.arch }} | |
| path: artifacts/packages/ | |
| - name: Set up .NET SDK | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| global-json-file: global.json | |
| - name: Resolve packed InitialForce.WPF version | |
| id: pkgver | |
| run: | | |
| $pkg = Get-ChildItem artifacts/packages -Recurse -Filter 'InitialForce.WPF.*.nupkg' | | |
| Where-Object { $_.Name -match '^InitialForce\.WPF\.\d' } | | |
| Select-Object -First 1 | |
| if (-not $pkg) { | |
| Write-Error "No InitialForce.WPF.<version>.nupkg under artifacts/packages — build-wpf did not pack." | |
| Get-ChildItem artifacts/packages -Recurse | Format-Table FullName | |
| exit 1 | |
| } | |
| $ver = $pkg.Name -replace '^InitialForce\.WPF\.', '' -replace '\.nupkg$', '' | |
| "version=$ver" | Out-File -Append $env:GITHUB_OUTPUT | |
| Write-Host "Resolved InitialForce.WPF version: $ver (from $($pkg.Name))" | |
| shell: pwsh | |
| - name: Run smoke harness against packed nupkg | |
| run: | | |
| $ver = "${{ steps.pkgver.outputs.version }}" | |
| $pkg = Get-ChildItem artifacts/packages -Recurse -Filter 'InitialForce.WPF.*.nupkg' | | |
| Where-Object { $_.Name -match '^InitialForce\.WPF\.\d' } | | |
| Select-Object -First 1 | |
| $feed = $pkg.DirectoryName | |
| Write-Host "Local packed feed: $feed" | |
| # Add the local packed feed and nuget.org onto the repo NuGet.config, which | |
| # already carries the pinned dnceng feeds the net10 private/servicing | |
| # transitive deps need. Using `dotnet nuget add source` (not stacked | |
| # `--source` flags) keeps the feed URL from being path-normalized into a | |
| # bogus local path (NU1301). | |
| dotnet nuget add source $feed --name local-packed --configfile NuGet.config | |
| dotnet nuget add source https://api.nuget.org/v3/index.json --name nuget.org --configfile NuGet.config | |
| dotnet restore test/InitialForce.WpfSmoke/InitialForce.WpfSmoke.csproj ` | |
| -p:InitialForceWpfVersion=$ver | |
| if ($LASTEXITCODE -ne 0) { throw "restore failed" } | |
| # Publish self-contained win-x64 (the csproj pins the RID + SelfContained), | |
| # which lays down a full runtime AND the patched WPF DLLs while stripping the | |
| # stock ones — the exact production consumption path, so the patched WPF loads | |
| # unambiguously. `dotnet test` cannot run a self-contained testhost from build | |
| # output (hostpolicy.dll is not laid down there), so run the NUnitLite console | |
| # entry point directly; it emits NUnit3 result XML. | |
| dotnet publish test/InitialForce.WpfSmoke/InitialForce.WpfSmoke.csproj ` | |
| -c Release --no-restore ` | |
| -p:InitialForceWpfVersion=$ver ` | |
| -o smoke-publish/ | |
| if ($LASTEXITCODE -ne 0) { throw "publish failed" } | |
| # The WPF Arcade SDK strips NuGet package assemblies from test-project output, | |
| # so the NUnit assemblies are absent from the self-contained publish. Copy them | |
| # from the restored package cache into the app directory; the self-contained host | |
| # probes the application base directory, so they load without a deps.json entry. | |
| $gp = ((dotnet nuget locals global-packages --list) -replace '^[^:]*:\s*','').Trim() | |
| Write-Host "NuGet global packages: $gp" | |
| foreach ($pkgId in 'nunit','nunitlite') { | |
| $pkgDir = Join-Path $gp $pkgId | |
| if (-not (Test-Path $pkgDir)) { throw "package cache missing '$pkgId' under $gp" } | |
| $verDir = Get-ChildItem $pkgDir -Directory | Sort-Object Name -Descending | Select-Object -First 1 | |
| $libRoot = Join-Path $verDir.FullName 'lib' | |
| $tfmDir = @('netstandard2.0','net6.0','net462') | | |
| ForEach-Object { Join-Path $libRoot $_ } | | |
| Where-Object { Test-Path $_ } | Select-Object -First 1 | |
| if (-not $tfmDir) { throw "no compatible lib TFM for '$pkgId' under $libRoot" } | |
| Get-ChildItem $tfmDir -Filter '*.dll' | ForEach-Object { | |
| Copy-Item $_.FullName smoke-publish/ -Force | |
| Write-Host " copied $($_.Name) from $pkgId ($($verDir.Name))" | |
| } | |
| } | |
| Write-Host "Publish output nunit assemblies:" | |
| Get-ChildItem smoke-publish -Filter 'nunit*' -ErrorAction SilentlyContinue | | |
| ForEach-Object { Write-Host " $($_.Name)" } | |
| # A self-contained app resolves assemblies strictly from its deps.json TPA and | |
| # does not probe the app base directory for files absent from it. The WPF Arcade | |
| # SDK strips the NUnit package assemblies, and the InitialForce.WPF targets strip | |
| # the patched WPF DLLs, from the generated deps.json (the WPF DLLs are still copied | |
| # into the app directory by the package targets, just not registered). Register | |
| # every assembly physically present in the publish directory that deps.json does | |
| # not already list, so the host resolves them from the app base directory. | |
| $depsPath = "smoke-publish/InitialForce.WpfSmoke.deps.json" | |
| $deps = Get-Content $depsPath -Raw | ConvertFrom-Json | |
| $rt = $deps.runtimeTarget.name | |
| $existing = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) | |
| foreach ($lib in $deps.targets.$rt.PSObject.Properties) { | |
| $runtime = $lib.Value.runtime | |
| if ($null -ne $runtime) { | |
| foreach ($r in $runtime.PSObject.Properties) { [void]$existing.Add([System.IO.Path]::GetFileName($r.Name)) } | |
| } | |
| } | |
| $appProp = $deps.targets.$rt.PSObject.Properties | | |
| Where-Object { $_.Name -like 'InitialForce.WpfSmoke/*' } | Select-Object -First 1 | |
| $runtimeObj = $deps.targets.$rt.($appProp.Name).runtime | |
| $added = 0 | |
| foreach ($f in Get-ChildItem smoke-publish -Filter '*.dll') { | |
| if (-not $existing.Contains($f.Name)) { | |
| $runtimeObj | Add-Member -NotePropertyName $f.Name -NotePropertyValue ([pscustomobject]@{}) -Force | |
| [void]$existing.Add($f.Name); $added++ | |
| Write-Host " deps.json += $($f.Name)" | |
| } | |
| } | |
| Write-Host "Registered $added assemblies in deps.json" | |
| ($deps | ConvertTo-Json -Depth 100) | Set-Content $depsPath -Encoding utf8 | |
| New-Item -ItemType Directory -Force -Path smoke-results | Out-Null | |
| & "smoke-publish/InitialForce.WpfSmoke.exe" --result:"smoke-results/smoke.xml" | |
| $runExit = $LASTEXITCODE | |
| Write-Host "NUnitLite exit code: $runExit" | |
| if ($runExit -ne 0) { throw "smoke run reported failures or errors (exit $runExit)" } | |
| shell: pwsh | |
| - name: Enforce non-empty smoke run (NUnit3 floor gate) | |
| if: always() | |
| run: | | |
| $floorFile = "test/InitialForce.WpfSmoke/expected-min-tests.txt" | |
| $floor = [int]((Get-Content $floorFile) | | |
| Where-Object { $_ -match '^\s*\d+' } | Select-Object -First 1) | |
| $xml = Get-ChildItem smoke-results -Recurse -Filter '*.xml' -ErrorAction SilentlyContinue | | |
| Select-Object -First 1 | |
| if (-not $xml) { | |
| Write-Error "No NUnit3 result XML produced — the smoke harness did not run (the fabricated-empty-results failure mode). Failing hard." | |
| exit 1 | |
| } | |
| [xml]$doc = Get-Content $xml.FullName | |
| $run = $doc.'test-run' | |
| $total = [int]$run.total | |
| Write-Host "Smoke results: total=$total passed=$($run.passed) failed=$($run.failed) (floor=$floor)" | |
| if ($total -lt $floor) { | |
| Write-Error "Smoke run reported $total tests, below the floor of $floor. Refusing to pass a hollow smoke run." | |
| exit 1 | |
| } | |
| if ([int]$run.failed -gt 0) { | |
| Write-Error "Smoke run reported $($run.failed) failed tests." | |
| exit 1 | |
| } | |
| shell: pwsh | |
| - name: Upload smoke results | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: smoke-${{ matrix.arch }}.xml | |
| path: smoke-results/ | |
| retention-days: 14 | |
| # --------------------------------------------------------------------------- | |
| # anti-stub-lint: ratchet-DOWN gate on placeholder Assert.That(true) stubs. | |
| # Source-level grep; runs independently of the Windows build. | |
| # --------------------------------------------------------------------------- | |
| anti-stub-lint: | |
| name: Anti-stub lint (smoke bodies) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Fail if placeholder stubs exceed the allowlist | |
| run: | | |
| set -euo pipefail | |
| # RATCHET-DOWN ceiling: the count of `Assert.That(true...` placeholder | |
| # stubs in the smoke bodies must never exceed MAX_STUBS. Lower it as | |
| # scenarios are un-stubbed; never raise it. Remaining stubs today are | |
| # FrugalList (2 — internal FrugalList<T> needs a test-friend adapter) | |
| # and PixelDiff (4 — needs golden PNGs committed on Windows). | |
| MAX_STUBS=6 | |
| count=$(grep -rn --include='*.cs' 'Assert.That(true' test/InitialForce.WpfSmoke/Smoke/ | wc -l | tr -d ' ') | |
| echo "Placeholder stubs found: $count (ceiling: $MAX_STUBS)" | |
| grep -rn --include='*.cs' 'Assert.That(true' test/InitialForce.WpfSmoke/Smoke/ || true | |
| if [ "$count" -gt "$MAX_STUBS" ]; then | |
| echo "::error::$count Assert.That(true) stubs exceed the ceiling of $MAX_STUBS. Un-stub the scenario (move the body out of the /* Full implementation */ block) or lower MAX_STUBS deliberately." | |
| exit 1 | |
| fi | |
| shell: bash | |
| # --------------------------------------------------------------------------- | |
| # perf: BenchmarkDotNet perf gate (needs build-wpf) | |
| # --------------------------------------------------------------------------- | |
| perf: | |
| name: Perf (${{ matrix.arch }}) | |
| runs-on: windows-2022 | |
| needs: build-wpf | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: [x64, arm64] | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: build-${{ matrix.arch }} | |
| path: artifacts/packages/ | |
| - name: Set up .NET SDK | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| global-json-file: global.json | |
| # Placeholder: run BenchmarkDotNet benchmarks. | |
| # TODO (wired by perf beads): replace with real benchmark invocation. | |
| - name: Run BenchmarkDotNet (placeholder) | |
| run: | | |
| echo "BenchmarkDotNet placeholder — wired by other beads." | |
| echo "Expected command:" | |
| echo " dotnet run --project test/InitialForce.WpfSmoke/ -c Release -- --filter '*Bench*' --exporters json" | |
| echo " python tools/check-regression.py --current-sha ${{ github.sha }} --series perf/series.jsonl" | |
| shell: bash | |
| - name: Upload perf results | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: perf-${{ matrix.arch }} | |
| path: BenchmarkDotNet.Artifacts/ | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| # --------------------------------------------------------------------------- | |
| # aggregate: diff smoke results, final gate (needs all prior jobs) | |
| # --------------------------------------------------------------------------- | |
| aggregate: | |
| name: Aggregate results | |
| runs-on: ubuntu-latest | |
| needs: [lint-tools, lint-yaml, anti-stub-lint, smoke, perf] | |
| if: always() | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - name: Install Python deps | |
| run: pip install pyyaml | |
| - name: Download smoke x64 results | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: smoke-x64.xml | |
| path: /tmp/smoke-results/x64/ | |
| continue-on-error: true | |
| - name: Download smoke arm64 results | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: smoke-arm64.xml | |
| path: /tmp/smoke-results/arm64/ | |
| continue-on-error: true | |
| - name: Download baseline smoke artifact (if available) | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: smoke-baseline | |
| path: /tmp/smoke-baseline/ | |
| continue-on-error: true | |
| - name: Diff smoke results (if baseline available) | |
| run: | | |
| if [ -d /tmp/smoke-baseline ] && [ "$(ls -A /tmp/smoke-baseline)" ]; then | |
| python tools/diff-smoke-results.py \ | |
| --upstream /tmp/smoke-baseline/ \ | |
| --fork /tmp/smoke-results/x64/ \ | |
| --output /tmp/smoke-diff-report.md | |
| echo "Smoke diff report:" | |
| cat /tmp/smoke-diff-report.md | |
| else | |
| echo "No baseline artifact found; skipping diff (first run or baseline not published)." | |
| fi | |
| shell: bash | |
| - name: Check all required jobs passed | |
| run: | | |
| LINT_TOOLS="${{ needs.lint-tools.result }}" | |
| LINT_YAML="${{ needs.lint-yaml.result }}" | |
| ANTI_STUB="${{ needs.anti-stub-lint.result }}" | |
| SMOKE="${{ needs.smoke.result }}" | |
| PERF="${{ needs.perf.result }}" | |
| echo "lint-tools: $LINT_TOOLS" | |
| echo "lint-yaml: $LINT_YAML" | |
| echo "anti-stub-lint: $ANTI_STUB" | |
| echo "smoke: $SMOKE" | |
| echo "perf: $PERF" | |
| FAILED=0 | |
| for result in "$LINT_TOOLS" "$LINT_YAML" "$ANTI_STUB" "$SMOKE" "$PERF"; do | |
| if [ "$result" != "success" ] && [ "$result" != "skipped" ]; then | |
| FAILED=1 | |
| fi | |
| done | |
| if [ "$FAILED" -eq 1 ]; then | |
| echo "::error::One or more required jobs failed. See details above." | |
| exit 1 | |
| fi | |
| echo "All required jobs passed." | |
| shell: bash |