Skip to content

Batch cherry-pick: 9 h3xds1nz upstream PRs (clean apply onto if/main) #71

Batch cherry-pick: 9 h3xds1nz upstream PRs (clean apply onto if/main)

Batch cherry-pick: 9 h3xds1nz upstream PRs (clean apply onto if/main) #71

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-latest
strategy:
fail-fast: false
matrix:
arch: [x64, arm64]
# arm64 cross-compile runs on windows-latest x64 runner; runs-on already pins to Windows.
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-latest 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
- 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: 22-scenario smoke harness (needs build-wpf)
# ---------------------------------------------------------------------------
smoke:
name: Smoke (${{ matrix.arch }})
runs-on: windows-latest
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 22-scenario smoke harness.
# TODO (wired by smoke-harness beads): replace with real test invocation.
- name: Run smoke harness (placeholder)
run: |
echo "Smoke harness placeholder — test/InitialForce.WpfSmoke/ wired by other beads."
echo "Expected command:"
echo " dotnet test test/InitialForce.WpfSmoke/ -c Release --logger trx --results-directory smoke-results/"
mkdir -p smoke-results
echo '<?xml version="1.0" encoding="UTF-8"?><TestRun id="placeholder"><Results/></TestRun>' > smoke-results/smoke-placeholder.xml
shell: bash
- name: Upload smoke results
uses: actions/upload-artifact@v4
with:
name: smoke-${{ matrix.arch }}.xml
path: smoke-results/
retention-days: 14
# ---------------------------------------------------------------------------
# perf: BenchmarkDotNet perf gate (needs build-wpf)
# ---------------------------------------------------------------------------
perf:
name: Perf (${{ matrix.arch }})
runs-on: windows-latest
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, 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 }}"
SMOKE="${{ needs.smoke.result }}"
PERF="${{ needs.perf.result }}"
echo "lint-tools: $LINT_TOOLS"
echo "lint-yaml: $LINT_YAML"
echo "smoke: $SMOKE"
echo "perf: $PERF"
FAILED=0
for result in "$LINT_TOOLS" "$LINT_YAML" "$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