Skip to content

ledger: discovered pr#8841 actor=claude-wpf-bot #962

ledger: discovered pr#8841 actor=claude-wpf-bot

ledger: discovered pr#8841 actor=claude-wpf-bot #962

Workflow file for this run

# .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