test: app exit code in ci #104
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: Windows CI | |
| on: | |
| push: | |
| branches: | |
| - master | |
| pull_request: | |
| branches: | |
| - master | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| id-token: write # Azure OIDC: the launch/exit smoke test signs the exe first, | |
| # because an unsigned uiAccess binary refuses to launch | |
| jobs: | |
| build: | |
| name: Windows build test (${{ matrix.arch }}) | |
| runs-on: ${{ matrix.runner }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - arch: amd64 | |
| runner: windows-latest | |
| triplet: x64-windows-static | |
| vcvars_arch: amd64 | |
| - arch: arm64 | |
| runner: windows-11-arm | |
| triplet: arm64-windows-static | |
| vcvars_arch: arm64 | |
| concurrency: | |
| group: windows-ci-${{ matrix.arch }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| defaults: | |
| run: | |
| shell: pwsh | |
| env: | |
| CLIPP_RELEASE_TRIPLET: ${{ matrix.triplet }} | |
| VCPKG_BINARY_SOURCES: clear;files,${{ github.workspace }}/vcpkg-cache,readwrite | |
| steps: | |
| - name: Checkout clipp | |
| uses: actions/checkout@v6 | |
| with: | |
| persist-credentials: false | |
| - name: Read vcpkg baseline | |
| id: vcpkg-baseline | |
| run: | | |
| $manifest = Get-Content -Raw src\vcpkg.json | ConvertFrom-Json | |
| $baseline = $manifest.'builtin-baseline' | |
| if (-not $baseline) { | |
| throw "src\vcpkg.json must define builtin-baseline for CI." | |
| } | |
| "ref=$baseline" >> $env:GITHUB_OUTPUT | |
| - name: Set vcpkg short root | |
| run: | | |
| $workspaceRoot = [System.IO.Path]::GetPathRoot($env:GITHUB_WORKSPACE) | |
| if (-not $workspaceRoot) { | |
| throw "Could not determine workspace drive from GITHUB_WORKSPACE='$env:GITHUB_WORKSPACE'." | |
| } | |
| $vcpkgRoot = Join-Path $workspaceRoot "v" | |
| if (Test-Path $vcpkgRoot) { | |
| throw "vcpkg short root already exists: $vcpkgRoot" | |
| } | |
| New-Item -ItemType Directory -Path $vcpkgRoot | Out-Null | |
| "VCPKG_ROOT=$vcpkgRoot" >> $env:GITHUB_ENV | |
| Write-Host "Using VCPKG_ROOT=$vcpkgRoot" | |
| - name: Checkout vcpkg | |
| run: | | |
| git clone --no-tags https://github.com/microsoft/vcpkg.git $env:VCPKG_ROOT | |
| git -C $env:VCPKG_ROOT checkout --detach ${{ steps.vcpkg-baseline.outputs.ref }} | |
| - name: Bootstrap vcpkg | |
| run: | | |
| $bootstrap = Join-Path $env:VCPKG_ROOT "bootstrap-vcpkg.bat" | |
| & $bootstrap -disableMetrics | |
| - name: Prepare vcpkg binary cache | |
| run: New-Item -ItemType Directory -Force vcpkg-cache | Out-Null | |
| - name: Cache vcpkg binaries | |
| uses: actions/cache@v5 | |
| with: | |
| path: vcpkg-cache | |
| key: ${{ runner.os }}-${{ runner.arch }}-vcpkg-${{ matrix.triplet }}-${{ hashFiles('src/vcpkg.json', 'src/vcpkg-triplets/**') }} | |
| restore-keys: | | |
| ${{ runner.os }}-${{ runner.arch }}-vcpkg-${{ matrix.triplet }}- | |
| - name: Build Release | |
| run: .\scripts\build_windows.ps1 -BuildType Release -Triplet $env:CLIPP_RELEASE_TRIPLET -VcVarsArch ${{ matrix.vcvars_arch }} -DisableCodeSigning | |
| - name: Verify Windows artifacts | |
| run: | | |
| $exe = ".\build\windows-release\clipp.exe" | |
| $shim = ".\build\windows-release\clipp.com" | |
| if (-not (Test-Path $exe)) { | |
| throw "Missing expected executable: $exe" | |
| } | |
| if (-not (Test-Path $shim)) { | |
| throw "Missing expected console shim: $shim" | |
| } | |
| - name: Verify static dependency closure | |
| run: | | |
| $exe = ".\build\windows-release\clipp.exe" | |
| $vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe" | |
| $installPath = & $vswhere -latest -products * -property installationPath | |
| $vcvars = Join-Path $installPath "VC\Auxiliary\Build\vcvarsall.bat" | |
| $dump = & cmd.exe /d /s /c "call `"$vcvars`" ${{ matrix.vcvars_arch }} >nul && dumpbin /dependents `"$exe`"" | |
| $dump | |
| $blocked = $dump | Select-String -Pattern "VCRUNTIME|MSVCP|ucrtbase|libsodium|xxhash|zstd|lodepng" -CaseSensitive:$false | |
| if ($blocked) { | |
| throw "Release exe imports non-system runtime/dependency DLLs: $($blocked -join '; ')" | |
| } | |
| # ---- launch/exit smoke test ------------------------------------------------- | |
| # Replays the winget validation harness's launch-and-quit sequence and fails on any | |
| # nonzero exit code — it treats one as a failed run (winget-pkgs PR #408205: Clipp | |
| # 1.4.0.150 came back "returned exit code: -1073741189" = 0xC000027B, | |
| # STATUS_STOWED_EXCEPTION; ../WM_NIGHT died the same way in #393605). | |
| # | |
| # This MUST test the shipped bits: build_windows.ps1 stamps uiAccess=true into every | |
| # Release exe, and an unsigned uiAccess binary REFUSES TO LAUNCH ("A referral was | |
| # returned from the server"). Stripping the stamp to make it launchable would test a | |
| # binary we never ship, so CI signs first — same Trusted Signing path as the release | |
| # workflow — and launches the real thing. | |
| # | |
| # Signing needs the AZURE_* secrets, which are absent on fork PRs. Those legs skip | |
| # the smoke test rather than fail: nothing to verify without a signature. | |
| - name: Can we sign? (smoke test needs a signed binary) | |
| id: cansign | |
| env: | |
| AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} | |
| run: | | |
| $ok = [bool]$env:AZURE_CLIENT_ID | |
| "ok=$($ok.ToString().ToLower())" >> $env:GITHUB_OUTPUT | |
| if (-not $ok) { | |
| Write-Host "::notice::No signing credentials (fork PR?) — skipping the launch/exit smoke test." | |
| } | |
| - name: Azure login (Trusted Signing) | |
| if: steps.cansign.outputs.ok == 'true' | |
| uses: azure/login@v2 | |
| with: | |
| client-id: ${{ secrets.AZURE_CLIENT_ID }} | |
| tenant-id: ${{ secrets.AZURE_TENANT_ID }} | |
| subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} | |
| - name: Sign Windows artifacts | |
| if: steps.cansign.outputs.ok == 'true' | |
| uses: azure/trusted-signing-action@v0 | |
| with: | |
| endpoint: ${{ vars.ARTIFACT_SIGNING_ENDPOINT }} | |
| trusted-signing-account-name: ${{ vars.ARTIFACT_SIGNING_ACCOUNT }} | |
| certificate-profile-name: ${{ vars.ARTIFACT_SIGNING_CERTIFICATE_PROFILE }} | |
| files-folder: ${{ github.workspace }}\build\windows-release | |
| files-folder-filter: exe,com | |
| file-digest: SHA256 | |
| timestamp-rfc3161: http://timestamp.acs.microsoft.com | |
| timestamp-digest: SHA256 | |
| # The action's dependency cache keys are arch-agnostic, so the amd64 and | |
| # arm64 matrix legs poison each other's TrustedSigning module cache | |
| # (Azure/trusted-signing-action#146). Fresh install costs ~1 min per leg. | |
| cache-dependencies: false | |
| - name: Verify signature and uiAccess stamp | |
| if: steps.cansign.outputs.ok == 'true' | |
| run: | | |
| $exe = ".\build\windows-release\clipp.exe" | |
| $sig = Get-AuthenticodeSignature $exe | |
| if ($sig.Status -ne 'Valid') { | |
| throw "Signature on $exe is not Valid: $($sig.Status) — $($sig.StatusMessage)" | |
| } | |
| Write-Host "[+] signed by: $($sig.SignerCertificate.Subject)" | |
| # Assert we are about to launch a uiAccess binary — the whole point is that the | |
| # shipped configuration is what gets exercised. | |
| $mt = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.*\*\mt.exe" -ErrorAction SilentlyContinue | | |
| Sort-Object FullName -Descending | Select-Object -First 1 | |
| if ($mt) { | |
| $manifest = Join-Path $env:RUNNER_TEMP 'shipped.manifest' | |
| & $mt.FullName -nologo -inputresource:"$exe;#1" -out:"$manifest" | Out-Null | |
| $xml = Get-Content -Raw $manifest | |
| if ($xml -match 'uiAccess="true"') { Write-Host '[+] uiAccess=true present (shipped configuration).' } | |
| else { Write-Host '::warning::uiAccess=true NOT present in the embedded manifest.' } | |
| } | |
| - name: Enable WER local crash dumps | |
| if: steps.cansign.outputs.ok == 'true' | |
| run: | | |
| # A stowed exception is a WinRT/COM fail-fast: it bypasses unhandled-exception | |
| # filters, so Clipp's own crash handler never sees it and writes nothing. WER | |
| # LocalDumps catches it at a lower level — the only way to get a dump at all. | |
| $dumps = Join-Path $env:RUNNER_TEMP 'werdumps' | |
| New-Item -ItemType Directory -Force $dumps | Out-Null | |
| $key = 'HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\clipp.exe' | |
| New-Item -Path $key -Force | Out-Null | |
| Set-ItemProperty -Path $key -Name DumpFolder -Value $dumps -Type ExpandString | |
| Set-ItemProperty -Path $key -Name DumpType -Value 2 -Type DWord # full dump | |
| Set-ItemProperty -Path $key -Name DumpCount -Value 5 -Type DWord | |
| "WER_DUMP_DIR=$dumps" >> $env:GITHUB_ENV | |
| # -RequireWindow: this runner has a virgin HKCU, so no group key exists and Clipp | |
| # must open its first-run XAML-Islands window — the exact path a validation VM takes, | |
| # and the one a developer box with a configured key never exercises. If the window | |
| # never appears, the island failed to come up: that is a failure, not a shrug. | |
| # Graceful path: tray-menu Exit runs main()'s full teardown and must return 0. | |
| - name: Smoke test — graceful exit (signed, uiAccess-stamped exe) | |
| if: steps.cansign.outputs.ok == 'true' | |
| run: .\scripts\exitcode_smoke.ps1 -ExePath ".\build\windows-release\clipp.exe" -RequireWindow -CloseMode TrayExit | |
| # Harness path: close the main window like Process.CloseMainWindow, then verify the | |
| # app stays resident instead of self-terminating. A 0xC000027B here IS the winget | |
| # validation failure reproduced — this is the leg that can actually catch it. | |
| - name: Smoke test — winget harness sequence | |
| if: steps.cansign.outputs.ok == 'true' | |
| run: .\scripts\exitcode_smoke.ps1 -ExePath ".\build\windows-release\clipp.exe" -RequireWindow -CloseMode Harness | |
| - name: Collect crash diagnostics | |
| if: failure() && steps.cansign.outputs.ok == 'true' | |
| run: | | |
| Write-Host '--- Application Error / WER events (last 30 min) ---' | |
| Get-WinEvent -FilterHashtable @{ | |
| LogName = 'Application'; StartTime = (Get-Date).AddMinutes(-30) | |
| } -ErrorAction SilentlyContinue | | |
| Where-Object { $_.ProviderName -in 'Application Error', 'Windows Error Reporting' -and | |
| $_.Message -match 'clipp' } | | |
| ForEach-Object { $_.TimeCreated; $_.Message; '---' } | |
| Write-Host "--- Dumps in $env:WER_DUMP_DIR ---" | |
| Get-ChildItem $env:WER_DUMP_DIR -ErrorAction SilentlyContinue | |
| - name: Upload crash dumps | |
| if: failure() && steps.cansign.outputs.ok == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: werdumps-${{ matrix.arch }} | |
| path: ${{ env.WER_DUMP_DIR }} | |
| if-no-files-found: ignore |