|
22 | 22 | - 'docs/**' |
23 | 23 | workflow_dispatch: |
24 | 24 |
|
| 25 | +permissions: |
| 26 | + contents: read |
| 27 | + pull-requests: write |
| 28 | + |
25 | 29 | concurrency: |
26 | 30 | group: pr-${{ github.event.pull_request.number || github.ref }} |
27 | 31 | cancel-in-progress: true |
@@ -135,6 +139,166 @@ jobs: |
135 | 139 | shell: pwsh |
136 | 140 | run: .\scripts\DoTests.ps1 |
137 | 141 |
|
| 142 | + - name: Generate API surface dumps |
| 143 | + id: apidump |
| 144 | + shell: pwsh |
| 145 | + run: | |
| 146 | + $winmdUtils = "bin\Release\net8.0\WinmdUtils.dll" |
| 147 | + $currentWinmd = "bin\Windows.Win32.winmd" |
| 148 | +
|
| 149 | + # Get the baseline winmd from the last release NuGet package |
| 150 | + . .\scripts\CommonUtils.ps1 |
| 151 | + $ErrorActionPreference = 'Continue' |
| 152 | + $PSNativeCommandUseErrorActionPreference = $false |
| 153 | + $baselineWinmd = Get-Win32MetadataLastReleaseWinmdPath |
| 154 | +
|
| 155 | + Write-Host "Baseline: $baselineWinmd" |
| 156 | + Write-Host "Current: $currentWinmd" |
| 157 | +
|
| 158 | + # Dump baseline |
| 159 | + Write-Host "Dumping baseline..." |
| 160 | + & dotnet $winmdUtils dump --winmd $baselineWinmd --output bin\baseline.apidump.cs |
| 161 | + if ($LASTEXITCODE -ne 0) { throw "Failed to dump baseline" } |
| 162 | +
|
| 163 | + # Dump current build |
| 164 | + Write-Host "Dumping current build..." |
| 165 | + & dotnet $winmdUtils dump --winmd $currentWinmd --output bin\current.apidump.cs |
| 166 | + if ($LASTEXITCODE -ne 0) { throw "Failed to dump current" } |
| 167 | +
|
| 168 | + # Generate diff — git diff exits 1 when differences exist, which is expected |
| 169 | + # Use cmd /c to isolate from PowerShell's native command error handling |
| 170 | + & cmd /c "git diff --no-index --unified=3 bin\baseline.apidump.cs bin\current.apidump.cs > bin\api-diff.patch 2>&1" |
| 171 | + $diffExitCode = $LASTEXITCODE |
| 172 | +
|
| 173 | + if ($diffExitCode -eq 0) { |
| 174 | + Write-Host "No API differences found." |
| 175 | + "has_diff=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append |
| 176 | + } else { |
| 177 | + Write-Host "API differences detected." |
| 178 | + "has_diff=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Append |
| 179 | +
|
| 180 | + # Count additions/deletions from the patch file |
| 181 | + $diffLines = Get-Content bin\api-diff.patch |
| 182 | + $additions = ($diffLines | Where-Object { $_ -match '^\+[^+]' }).Count |
| 183 | + $deletions = ($diffLines | Where-Object { $_ -match '^\-[^-]' }).Count |
| 184 | + "additions=$additions" | Out-File -FilePath $env:GITHUB_OUTPUT -Append |
| 185 | + "deletions=$deletions" | Out-File -FilePath $env:GITHUB_OUTPUT -Append |
| 186 | +
|
| 187 | + Write-Host "+$additions additions / -$deletions deletions" |
| 188 | +
|
| 189 | + # Truncate if too large for a GH comment |
| 190 | + $diffText = $diffLines -join "`n" |
| 191 | + $maxLen = 60000 |
| 192 | + if ($diffText.Length -gt $maxLen) { |
| 193 | + $diffText = $diffText.Substring(0, $maxLen) + "`n`n... (diff truncated - see full artifact)" |
| 194 | + Set-Content -Path bin\api-diff.patch -Value $diffText -Encoding UTF8 |
| 195 | + } |
| 196 | + } |
| 197 | +
|
| 198 | + # Reset LASTEXITCODE so GitHub Actions doesn't treat this step as failed |
| 199 | + # (git diff exits 1 when differences exist, which is expected/success for us) |
| 200 | + $global:LASTEXITCODE = 0 |
| 201 | +
|
| 202 | + - name: Post API diff comment on PR |
| 203 | + if: github.event_name == 'pull_request' && steps.apidump.outputs.has_diff == 'true' |
| 204 | + uses: actions/github-script@v7 |
| 205 | + with: |
| 206 | + script: | |
| 207 | + const fs = require('fs'); |
| 208 | + const diff = fs.readFileSync('bin/api-diff.patch', 'utf8'); |
| 209 | + const prNumber = context.payload.pull_request.number; |
| 210 | + const additions = ${{ steps.apidump.outputs.additions || 0 }}; |
| 211 | + const deletions = ${{ steps.apidump.outputs.deletions || 0 }}; |
| 212 | + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; |
| 213 | +
|
| 214 | + const marker = '<!-- win32metadata-api-diff -->'; |
| 215 | + const body = `${marker} |
| 216 | + ## 📋 API Surface Diff |
| 217 | +
|
| 218 | + **+${additions} additions / -${deletions} deletions** vs last release ([full build log](${runUrl})) |
| 219 | +
|
| 220 | + <details> |
| 221 | + <summary>Click to expand API diff</summary> |
| 222 | +
|
| 223 | + \`\`\`diff |
| 224 | + ${diff} |
| 225 | + \`\`\` |
| 226 | +
|
| 227 | + </details> |
| 228 | +
|
| 229 | + > This comment is automatically updated on each push.`; |
| 230 | +
|
| 231 | + const comments = await github.rest.issues.listComments({ |
| 232 | + owner: context.repo.owner, |
| 233 | + repo: context.repo.repo, |
| 234 | + issue_number: prNumber, |
| 235 | + per_page: 100 |
| 236 | + }); |
| 237 | +
|
| 238 | + const existing = comments.data.find(c => c.body.includes(marker)); |
| 239 | +
|
| 240 | + if (existing) { |
| 241 | + await github.rest.issues.updateComment({ |
| 242 | + owner: context.repo.owner, |
| 243 | + repo: context.repo.repo, |
| 244 | + comment_id: existing.id, |
| 245 | + body: body |
| 246 | + }); |
| 247 | + } else { |
| 248 | + await github.rest.issues.createComment({ |
| 249 | + owner: context.repo.owner, |
| 250 | + repo: context.repo.repo, |
| 251 | + issue_number: prNumber, |
| 252 | + body: body |
| 253 | + }); |
| 254 | + } |
| 255 | +
|
| 256 | + - name: Post no-diff comment on PR |
| 257 | + if: github.event_name == 'pull_request' && steps.apidump.outputs.has_diff == 'false' |
| 258 | + uses: actions/github-script@v7 |
| 259 | + with: |
| 260 | + script: | |
| 261 | + const prNumber = context.payload.pull_request.number; |
| 262 | + const marker = '<!-- win32metadata-api-diff -->'; |
| 263 | + 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.`; |
| 264 | +
|
| 265 | + const comments = await github.rest.issues.listComments({ |
| 266 | + owner: context.repo.owner, |
| 267 | + repo: context.repo.repo, |
| 268 | + issue_number: prNumber, |
| 269 | + per_page: 100 |
| 270 | + }); |
| 271 | +
|
| 272 | + const existing = comments.data.find(c => c.body.includes(marker)); |
| 273 | +
|
| 274 | + if (existing) { |
| 275 | + await github.rest.issues.updateComment({ |
| 276 | + owner: context.repo.owner, |
| 277 | + repo: context.repo.repo, |
| 278 | + comment_id: existing.id, |
| 279 | + body: body |
| 280 | + }); |
| 281 | + } else { |
| 282 | + await github.rest.issues.createComment({ |
| 283 | + owner: context.repo.owner, |
| 284 | + repo: context.repo.repo, |
| 285 | + issue_number: prNumber, |
| 286 | + body: body |
| 287 | + }); |
| 288 | + } |
| 289 | +
|
| 290 | + - name: Upload winmd and API dump |
| 291 | + uses: actions/upload-artifact@v4 |
| 292 | + if: always() |
| 293 | + with: |
| 294 | + name: winmd |
| 295 | + path: | |
| 296 | + bin/Windows.Win32.winmd |
| 297 | + bin/current.apidump.cs |
| 298 | + bin/baseline.apidump.cs |
| 299 | + bin/api-diff.patch |
| 300 | + retention-days: 30 |
| 301 | + |
138 | 302 | - name: Upload build logs |
139 | 303 | uses: actions/upload-artifact@v4 |
140 | 304 | if: always() |
|
0 commit comments