Add RAII for PIDL types (BSTR-style) and FreeWith(CoTaskMemFree) for CoTaskMem string out-params #79
Workflow file for this run
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
| # PR Validation — Full cross-architecture build and test | |
| # | |
| # Mirrors the Azure Pipelines CI pipeline (azure-pipelines.yml) but runs as a | |
| # GitHub Actions workflow so PR checks work without Azure DevOps integration. | |
| # | |
| # Pipeline structure: | |
| # 1. Three parallel scrape jobs (x64, x86, arm64) generate metadata source | |
| # 2. One build job assembles the winmd, packages, samples, and runs tests | |
| name: PR Validation | |
| on: | |
| pull_request: | |
| branches: [main] | |
| paths-ignore: | |
| - 'apidocs/**' | |
| - 'docs/**' | |
| push: | |
| branches: [main] | |
| paths-ignore: | |
| - 'apidocs/**' | |
| - 'docs/**' | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| concurrency: | |
| group: pr-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| BUILD_CONFIGURATION: Release | |
| jobs: | |
| # ────────────────────────────────────────────────────────────────────── | |
| # Stage 1: Parallel header scraping (one job per architecture) | |
| # ────────────────────────────────────────────────────────────────────── | |
| scrape: | |
| name: 'Scrape headers: ${{ matrix.arch }}' | |
| runs-on: windows-2022 | |
| timeout-minutes: 60 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| arch: [x64, x86, arm64] | |
| include: | |
| - arch: x64 | |
| extra_args: '' | |
| set_version: true | |
| - arch: x86 | |
| extra_args: '-scrapeConstants' | |
| set_version: false | |
| - arch: arm64 | |
| extra_args: '' | |
| set_version: false | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| lfs: true | |
| fetch-depth: 0 | |
| - uses: actions/setup-dotnet@v4 | |
| with: | |
| global-json-file: global.json | |
| - name: Install nbgv and set version | |
| if: matrix.set_version | |
| shell: pwsh | |
| run: | | |
| .\scripts\Install-DotNetTool.ps1 -Name nbgv -NuGetConfigFile "${{ github.workspace }}\nuget.config" | |
| nbgv cloud | |
| - name: Generate metadata source (${{ matrix.arch }}) | |
| shell: pwsh | |
| run: .\scripts\GenerateMetadataSource.ps1 -arch ${{ matrix.arch }} ${{ matrix.extra_args }} | |
| - name: Upload generated assets | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: generated_${{ matrix.arch }} | |
| path: generation/WinSDK/obj | |
| retention-days: 1 | |
| - name: Upload build logs | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: scrape_logs_${{ matrix.arch }} | |
| path: bin/logs | |
| retention-days: 7 | |
| # ────────────────────────────────────────────────────────────────────── | |
| # Stage 2: Build, test, and package (depends on all scrape jobs) | |
| # ────────────────────────────────────────────────────────────────────── | |
| build-test: | |
| name: 'Build, test, package' | |
| needs: [scrape] | |
| runs-on: windows-2022 | |
| timeout-minutes: 60 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| lfs: true | |
| fetch-depth: 0 | |
| - uses: actions/setup-dotnet@v4 | |
| with: | |
| global-json-file: global.json | |
| - uses: microsoft/setup-msbuild@v2 | |
| - name: Download all generated assets | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: generated_* | |
| path: generation/WinSDK/obj | |
| merge-multiple: true | |
| - name: Build metadata binary | |
| shell: pwsh | |
| run: .\scripts\BuildMetadataBin.ps1 -assetsScrapedSeparately | |
| - name: Package | |
| shell: pwsh | |
| run: .\scripts\DoPackages.ps1 | |
| - name: Build samples | |
| shell: pwsh | |
| run: .\scripts\DoSamples.ps1 | |
| - name: Run tests | |
| shell: pwsh | |
| run: .\scripts\DoTests.ps1 | |
| - name: Download baseline winmd from target branch | |
| if: github.event_name == 'pull_request' | |
| id: baseline | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const path = require('path'); | |
| // Find the latest successful run on the target branch | |
| const baseBranch = context.payload.pull_request.base.ref; | |
| const runs = await github.rest.actions.listWorkflowRuns({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| workflow_id: 'pr-validation.yml', | |
| branch: baseBranch, | |
| status: 'success', | |
| per_page: 1 | |
| }); | |
| if (runs.data.workflow_runs.length === 0) { | |
| core.warning(`No successful runs found on ${baseBranch}. Will fall back to NuGet baseline.`); | |
| return; | |
| } | |
| const runId = runs.data.workflow_runs[0].id; | |
| core.info(`Found baseline run ${runId} on ${baseBranch}`); | |
| // Find the winmd artifact | |
| const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: runId | |
| }); | |
| const winmdArtifact = artifacts.data.artifacts.find(a => a.name === 'winmd'); | |
| if (!winmdArtifact) { | |
| core.warning('No winmd artifact found in baseline run. Will fall back to NuGet baseline.'); | |
| return; | |
| } | |
| // Download and extract | |
| const download = await github.rest.actions.downloadArtifact({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| artifact_id: winmdArtifact.id, | |
| archive_format: 'zip' | |
| }); | |
| const outputDir = 'bin/baseline-artifact'; | |
| fs.mkdirSync(outputDir, { recursive: true }); | |
| const zipPath = path.join(outputDir, 'artifact.zip'); | |
| fs.writeFileSync(zipPath, Buffer.from(download.data)); | |
| // Extract using PowerShell | |
| const { execSync } = require('child_process'); | |
| execSync(`Expand-Archive -Path "${zipPath}" -DestinationPath "${outputDir}" -Force`, { shell: 'pwsh' }); | |
| fs.unlinkSync(zipPath); | |
| core.info(`Extracted baseline winmd to ${outputDir}`); | |
| - name: Generate API surface diff | |
| if: github.event_name == 'pull_request' | |
| id: apidump | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = 'Continue' | |
| $PSNativeCommandUseErrorActionPreference = $false | |
| $winmdUtils = "bin\Release\net8.0\WinmdUtils.dll" | |
| $currentWinmd = "bin\Windows.Win32.winmd" | |
| # Determine baseline: prefer main branch artifact, fall back to last release | |
| $baselineWinmd = "bin\baseline-artifact\Windows.Win32.winmd" | |
| if (-not (Test-Path $baselineWinmd)) { | |
| Write-Host "No main branch artifact found, falling back to last NuGet release..." | |
| . .\scripts\CommonUtils.ps1 | |
| $baselineWinmd = Get-Win32MetadataLastReleaseWinmdPath | |
| } | |
| Write-Host "Baseline: $baselineWinmd" | |
| Write-Host "Current: $currentWinmd" | |
| # Dump baseline | |
| Write-Host "Dumping baseline..." | |
| & dotnet $winmdUtils dump --winmd $baselineWinmd --output bin\baseline.apidump.cs | |
| if ($LASTEXITCODE -ne 0) { throw "Failed to dump baseline" } | |
| # Dump current build | |
| Write-Host "Dumping current build..." | |
| & dotnet $winmdUtils dump --winmd $currentWinmd --output bin\current.apidump.cs | |
| if ($LASTEXITCODE -ne 0) { throw "Failed to dump current" } | |
| # Generate diff — git diff exits 1 when differences exist, which is expected | |
| & cmd /c "git diff --no-index --unified=3 bin\baseline.apidump.cs bin\current.apidump.cs > bin\api-diff.patch 2>&1" | |
| $diffExitCode = $LASTEXITCODE | |
| if ($diffExitCode -eq 0) { | |
| Write-Host "No API differences found." | |
| "has_diff=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append | |
| } else { | |
| Write-Host "API differences detected." | |
| "has_diff=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Append | |
| # Count additions/deletions from the patch file | |
| $diffLines = Get-Content bin\api-diff.patch | |
| $additions = ($diffLines | Where-Object { $_ -match '^\+[^+]' }).Count | |
| $deletions = ($diffLines | Where-Object { $_ -match '^\-[^-]' }).Count | |
| "additions=$additions" | Out-File -FilePath $env:GITHUB_OUTPUT -Append | |
| "deletions=$deletions" | Out-File -FilePath $env:GITHUB_OUTPUT -Append | |
| Write-Host "+$additions additions / -$deletions deletions" | |
| # Truncate if too large for a GH comment | |
| $diffText = $diffLines -join "`n" | |
| $maxLen = 60000 | |
| if ($diffText.Length -gt $maxLen) { | |
| $diffText = $diffText.Substring(0, $maxLen) + "`n`n... (diff truncated - see full artifact)" | |
| Set-Content -Path bin\api-diff.patch -Value $diffText -Encoding UTF8 | |
| } | |
| } | |
| # Reset LASTEXITCODE so GitHub Actions doesn't treat this step as failed | |
| $global:LASTEXITCODE = 0 | |
| - name: Post API diff comment on PR | |
| if: github.event_name == 'pull_request' && steps.apidump.outputs.has_diff == 'true' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const diff = fs.readFileSync('bin/api-diff.patch', 'utf8'); | |
| const prNumber = context.payload.pull_request.number; | |
| const additions = ${{ steps.apidump.outputs.additions || 0 }}; | |
| const deletions = ${{ steps.apidump.outputs.deletions || 0 }}; | |
| const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const marker = '<!-- win32metadata-api-diff -->'; | |
| const body = `${marker} | |
| ## 📋 API Surface Diff | |
| **+${additions} additions / -${deletions} deletions** vs main branch ([full build log](${runUrl})) | |
| <details> | |
| <summary>Click to expand API diff</summary> | |
| \`\`\`diff | |
| ${diff} | |
| \`\`\` | |
| </details> | |
| > This comment is automatically updated on each push.`; | |
| const comments = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| per_page: 100 | |
| }); | |
| const existing = comments.data.find(c => c.body.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body: body | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: body | |
| }); | |
| } | |
| - name: Post no-diff comment on PR | |
| if: github.event_name == 'pull_request' && steps.apidump.outputs.has_diff == 'false' | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const prNumber = context.payload.pull_request.number; | |
| const marker = '<!-- win32metadata-api-diff -->'; | |
| const body = `${marker}\n## 📋 API Surface Diff\n\n✅ No API differences vs last release.\n\n> This comment is automatically updated on each push.`; | |
| const comments = await github.rest.issues.listComments({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| per_page: 100 | |
| }); | |
| const existing = comments.data.find(c => c.body.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.updateComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| body: body | |
| }); | |
| } else { | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: prNumber, | |
| body: body | |
| }); | |
| } | |
| - name: Upload winmd and API dump | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: winmd | |
| path: | | |
| bin/Windows.Win32.winmd | |
| bin/current.apidump.cs | |
| bin/baseline.apidump.cs | |
| bin/api-diff.patch | |
| retention-days: 30 | |
| - name: Upload build logs | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: build_logs | |
| path: bin/logs | |
| retention-days: 7 | |
| - name: Upload NuGet packages | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: NuGetPackages | |
| path: bin/Packages/Release/NuGet | |
| retention-days: 7 |