From abe60d9bbda2bacf245da84d5488e754df98ed41 Mon Sep 17 00:00:00 2001 From: Jevan Saks Date: Wed, 24 Jun 2026 22:30:50 -0700 Subject: [PATCH 1/3] Replace changelog system with automated API diff for PR review Remove the ChangesSinceLastRelease.txt system (which caused merge conflicts and lost history on every release) in favor of an automated approach: 1. WinmdUtils 'dump' command: Decompiles a winmd to sorted C# declarations using ICSharpCode.Decompiler, producing deterministic output suitable for text diffing. Shows full type declarations with attributes, method signatures, constants, and nested types. 2. Local diff script (scripts/DiffWinmdToBaseline.ps1): Dumps both the current build and last-release winmd, shows a unified diff via git diff. 3. CI artifact: The PR Validation workflow now publishes the winmd and its API dump as a 'winmd' artifact (retained 30 days). 4. PR API Diff workflow (.github/workflows/pr-api-diff.yml): Triggers after PR Validation succeeds, generates the baseline dump, diffs against the PR's dump, and posts/updates a comment on the PR with the full diff in a collapsible section. Removed: - scripts/ChangesSinceLastRelease.txt (no longer needed) - scripts/UpdateChangesSinceLastRelease.ps1 (no longer needed) - The build no longer fails on API diffs; reviewer sees them in PR comment Changed: - TestWinmdBinary.ps1: Compare is now informational (non-failing) - CompareBinToLastRelease.ps1: Simplified (no knownDiffs/update logic) - Set-LastReleaseVersion.ps1: Removed file-clearing logic Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 2 +- .github/workflows/pr-validation.yml | 164 ++++++++++++++++++ CONTRIBUTING.md | 20 ++- .../win32metadata-detailed-research.md | 2 +- .../win32metadata-research-summary.md | 2 +- scripts/ChangesSinceLastRelease.txt | 88 ---------- scripts/CommonUtils.ps1 | 5 - scripts/CompareBinToLastRelease.ps1 | 17 +- scripts/DiffWinmdToBaseline.ps1 | 85 +++++++++ scripts/Set-LastReleaseVersion.ps1 | 6 - scripts/TestWinmdBinary.ps1 | 12 +- scripts/UpdateChangesSinceLastRelease.ps1 | 9 - sources/WinmdUtils/Program.cs | 111 ++++++++++++ .../WinmdUtils/Properties/launchSettings.json | 6 +- 14 files changed, 391 insertions(+), 138 deletions(-) delete mode 100644 scripts/ChangesSinceLastRelease.txt create mode 100644 scripts/DiffWinmdToBaseline.ps1 delete mode 100644 scripts/UpdateChangesSinceLastRelease.ps1 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7c3e75261..36b474390 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -66,7 +66,7 @@ Each arch job scrapes ALL partitions for that architecture. **NuGet feed:** The repo uses a private ADO Artifact Feed (`Win32Metadata-Dependencies`) as its sole package source. New packages from nuget.org must be saved to this feed first. -**Winmd equivalence:** After any change, the winmd must be compared against the baseline. The `NoSuggestedRemappings` test in `Windows.Win32.Tests` validates that no new unhandled remappings were introduced. Changes to the winmd are tracked in `scripts/ChangesSinceLastRelease.txt`. +**Winmd equivalence:** After any change, the winmd must be compared against the baseline. The `NoSuggestedRemappings` test in `Windows.Win32.Tests` validates that no new unhandled remappings were introduced. API diffs are automatically posted as PR comments by CI. Run `.\scripts\DiffWinmdToBaseline.ps1` locally to see a full C# declaration diff against the last release. **Cross-partition remaps:** Remaps in `scraper.settings.rsp` are global — they apply to all partitions. Auto-discovered remaps (from `Win32MetadataScraper`) are per-partition. If a tag name is used across partitions but the typedef is only in one partition's headers, the remap must be global (in `scraper.settings.rsp`) OR the referencing partition must `#include` the header with the typedef. diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 87fcbe177..7cbf48d4c 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -22,6 +22,10 @@ on: - 'docs/**' workflow_dispatch: +permissions: + contents: read + pull-requests: write + concurrency: group: pr-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true @@ -135,6 +139,166 @@ jobs: shell: pwsh run: .\scripts\DoTests.ps1 + - name: Generate API surface dumps + id: apidump + shell: pwsh + run: | + $winmdUtils = "bin\Release\net8.0\WinmdUtils.dll" + $currentWinmd = "bin\Windows.Win32.winmd" + + # Get the baseline winmd from the last release NuGet package + . .\scripts\CommonUtils.ps1 + $ErrorActionPreference = 'Continue' + $PSNativeCommandUseErrorActionPreference = $false + $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 + # Use cmd /c to isolate from PowerShell's native command error handling + & 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 + # (git diff exits 1 when differences exist, which is expected/success for us) + $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 = ''; + const body = `${marker} + ## šŸ“‹ API Surface Diff + + **+${additions} additions / -${deletions} deletions** vs last release ([full build log](${runUrl})) + +
+ Click to expand API diff + + \`\`\`diff + ${diff} + \`\`\` + +
+ + > 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 = ''; + 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() diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ee293b249..dce7de619 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -252,11 +252,23 @@ Run `./DoAll.ps1 -ExcludePackages -ExcludeSamples` in [PowerShell 7](https://aka Note that stale artifacts on your system may sometimes result in cryptic errors when attempting incremental builds. If you do encounter cryptic errors during incremental builds that you suspect are the result of previously built changes, reset your system state by running a clean build with `./DoAll.ps1 -Clean`. -### Comparing against the last release +### Reviewing API changes -A list of accumulated changes since the last release is kept at [ChangesSinceLastRelease.txt](scripts/ChangesSinceLastRelease.txt). New changes are reported by `./scripts/TestWinmdBinary.ps1` which is called during both full and incremental builds if you follow the steps above. +API differences between your build and the last release are automatically detected during the build. When you submit a PR, the CI pipeline posts a comment showing the full API diff as decompiled C# declarations, making it easy for reviewers to see exactly what changed. -When validating changes, it's important to evaluate the diffs to ensure all changes are intentional. Common patterns to expect in the diffs include: +#### Viewing changes locally + +To see a full diff of your changes locally before submitting a PR: + +```powershell +.\scripts\DiffWinmdToBaseline.ps1 +``` + +This decompiles both the current build's winmd and the last released winmd to sorted C# declarations, then displays a unified diff. The output shows full type declarations with attributes, method signatures, and constants — similar to what you'd see in ILSpy. + +#### What to look for + +When validating changes, it's important to evaluate the diffs to ensure all changes are intentional. Common patterns to expect include: * APIs were added to the baseline * APIs were removed from the baseline @@ -264,8 +276,6 @@ When validating changes, it's important to evaluate the diffs to ensure all chan Additionally, it is useful to load the winmd in [ILSpy](https://github.com/icsharpcode/ILSpy) and navigate through the APIs as another means to identify additional changes that may be required to achieve the desired end result. You may notice that two related APIs are in different namespaces or that a type that an API depends on was not moved as you would have expected. If that happens, search the repo for the API or its header file to identify where it may be being mapped to another namespace. -Once all the changes are validated, update the list of known changes since the last release by following the steps reported in the build output. When a new release is made, the list of changes in [ChangesSinceLastRelease.txt](scripts/ChangesSinceLastRelease.txt) will get reset and will start accumulating again until the next release. - ## Releasing The main branch must have a clean build to publish a new release. Run the [release pipeline](https://github-private.visualstudio.com/microsoft/_build?definitionId=750) to publish new packages to nuget.org and create a new draft release on GitHub autopopulated with the list of resolved issues. diff --git a/docs/copilot/research/win32metadata-detailed-research.md b/docs/copilot/research/win32metadata-detailed-research.md index 327e14c0c..7f5c654e8 100644 --- a/docs/copilot/research/win32metadata-detailed-research.md +++ b/docs/copilot/research/win32metadata-detailed-research.md @@ -475,7 +475,7 @@ DoAll.ps1 6. **Allow-list testing pattern**: Tests use `.rsp` allow-list files to track known acceptable violations. This enables progressive quality improvement without blocking builds. -7. **Winmd diff as release gate**: Every build compares the new .winmd against the last release via `WinmdUtils compare`. The diff is tracked in `ChangesSinceLastRelease.txt` to ensure all changes are intentional. +7. **Winmd diff for PR review**: Every build compares the new .winmd against the last release via `WinmdUtils compare`. The full API diff (decompiled C# declarations) is posted as a PR comment by CI for reviewer visibility. 8. **Separate pipelines for metadata vs docs**: The API documentation pipeline (`azure-pipelines-apidocs.yml`) is completely independent, triggered only by changes in `apidocs/`. diff --git a/docs/copilot/research/win32metadata-research-summary.md b/docs/copilot/research/win32metadata-research-summary.md index ab650e73d..0e36b1f73 100644 --- a/docs/copilot/research/win32metadata-research-summary.md +++ b/docs/copilot/research/win32metadata-research-summary.md @@ -123,7 +123,7 @@ Tasks are loaded via `UsingTask AssemblyFile` (in-process), but net8.0 assemblie - No full winmd binary snapshots (only ~100 interfaces) - No cross-build consistency tests ("build A vs build B") - No hash-based regression detection -- `ChangesSinceLastRelease.txt` is manually maintained (400+ lines) +- API diffs are posted as PR comments (full C# declarations via `WinmdUtils dump`) ### 3.5 Metadata Defined After-the-Fact diff --git a/scripts/ChangesSinceLastRelease.txt b/scripts/ChangesSinceLastRelease.txt deleted file mode 100644 index 2133e3508..000000000 --- a/scripts/ChangesSinceLastRelease.txt +++ /dev/null @@ -1,88 +0,0 @@ -# Upgrade D3D12 Agility SDK to 1.618.5 -Windows.Win32.Graphics.Direct3D12.Apis.D3D12_MS_DISPATCH_MAX_THREAD_GROUPS_PER_GRID added -Windows.Win32.Graphics.Direct3D12.Apis.D3D12_WORK_GRAPHS_DISPATCH_MAX_THREAD_GROUPS_PER_GRID added -# Upgrade D3D12 Agility SDK to 1.619.0 -Windows.Win32.Graphics.Direct3D12.Apis.CLSID_D3D12ApplicationIdentity added -Windows.Win32.Graphics.Direct3D12.Apis.CLSID_D3D12RuntimeValidationControl added -Windows.Win32.Graphics.Direct3D12.Apis.D3D12_AS_TGSM_BYTES_MINIMUM_SUPPORT added -Windows.Win32.Graphics.Direct3D12.Apis.D3D12_CS_TGSM_BYTES_MINIMUM_SUPPORT added -Windows.Win32.Graphics.Direct3D12.Apis.D3D12_GUID_TEXTURE_LAYOUT_ROW_MAJOR_HEIGHT_ALIGNMENT added -Windows.Win32.Graphics.Direct3D12.Apis.D3D12_GUID_TEXTURE_LAYOUT_ROW_MAJOR_PITCH_ALIGNMENT added -Windows.Win32.Graphics.Direct3D12.Apis.D3D12_GUID_TEXTURE_LAYOUT_ROW_MAJOR_PLANE_ALIGNMENT added -Windows.Win32.Graphics.Direct3D12.Apis.D3D12_MS_TGSM_BYTES_MINIMUM_SUPPORT added -Windows.Win32.Graphics.Direct3D12.Apis.D3D12TiledResourceTier4 removed -Windows.Win32.Graphics.Direct3D12.D3D_SHADER_MODEL.D3D_SHADER_MODEL_6_10 added -Windows.Win32.Graphics.Direct3D12.D3D12_BUFFER_SRV_BYTE_OFFSET added -Windows.Win32.Graphics.Direct3D12.D3D12_BUFFER_UAV_BYTE_OFFSET added -Windows.Win32.Graphics.Direct3D12.D3D12_CREATE_STATE_OBJECT_STATISTICS added -Windows.Win32.Graphics.Direct3D12.D3D12_FEATURE_DATA_BARRIER_LAYOUT added -Windows.Win32.Graphics.Direct3D12.D3D12_FEATURE_DATA_D3D12_OPTIONS22 added -Windows.Win32.Graphics.Direct3D12.D3D12_FEATURE.D3D12_FEATURE_BARRIER_LAYOUT added -Windows.Win32.Graphics.Direct3D12.D3D12_FEATURE.D3D12_FEATURE_D3D12_OPTIONS22 added -Windows.Win32.Graphics.Direct3D12.D3D12_MESSAGE_ID.D3D12_MESSAGE_ID_CREATEQUERYHEAP_INVALID_FLAGS added -Windows.Win32.Graphics.Direct3D12.D3D12_MESSAGE_ID.D3D12_MESSAGE_ID_GPU_BASED_VALIDATION_NON_UNIFORM_RESOURCE_INDEX added -Windows.Win32.Graphics.Direct3D12.D3D12_MESSAGE_ID.D3D12_MESSAGE_ID_RESOLVEQUERYDATA_INVALID_QUERYHEAP added -Windows.Win32.Graphics.Direct3D12.D3D12_MESSAGE_ID.D3D12_MESSAGE_ID_STOREPIPELINE_CACHED_BLOB_DISABLED added -Windows.Win32.Graphics.Direct3D12.D3D12_PFN_TRIM_NOTIFICATION_CALLBACK added -Windows.Win32.Graphics.Direct3D12.D3D12_PIPELINE_STATE_FLAGS.D3D12_PIPELINE_STATE_FLAG_DISABLE_CACHED_BLOB added -Windows.Win32.Graphics.Direct3D12.D3D12_QUERY_HEAP_FLAGS added -Windows.Win32.Graphics.Direct3D12.D3D12_QUERY_HEAP_FLAGS.D3D12_QUERY_HEAP_FLAG_CPU_RESOLVE added -Windows.Win32.Graphics.Direct3D12.D3D12_QUERY_HEAP_FLAGS.D3D12_QUERY_HEAP_FLAG_NONE added -Windows.Win32.Graphics.Direct3D12.D3D12_REGISTER_TRIM_NOTIFICATION added -Windows.Win32.Graphics.Direct3D12.D3D12_SHADER_RESOURCE_VIEW_DESC._Anonymous_e__Union.BufferByteOffset added -Windows.Win32.Graphics.Direct3D12.D3D12_SRV_DIMENSION.D3D12_SRV_DIMENSION_BUFFER_BYTE_OFFSET added -Windows.Win32.Graphics.Direct3D12.D3D12_STATE_OBJECT_STATISTICS added -Windows.Win32.Graphics.Direct3D12.D3D12_STATE_SUBOBJECT_TYPE.D3D12_STATE_SUBOBJECT_TYPE_COMPILER_EXISITING_COLLECTION removed -Windows.Win32.Graphics.Direct3D12.D3D12_STATE_SUBOBJECT_TYPE.D3D12_STATE_SUBOBJECT_TYPE_COMPILER_EXISTING_COLLECTION added -Windows.Win32.Graphics.Direct3D12.D3D12_TRIM_NOTIFICATION added -Windows.Win32.Graphics.Direct3D12.D3D12_TRIM_NOTIFICATION_FLAGS added -Windows.Win32.Graphics.Direct3D12.D3D12_TRIM_NOTIFICATION_FLAGS.D3D12_TRIM_NOTIFICATION_FLAG_NONE added -Windows.Win32.Graphics.Direct3D12.D3D12_TRIM_NOTIFICATION_FLAGS.D3D12_TRIM_NOTIFICATION_FLAG_PERIODIC_TRIM added -Windows.Win32.Graphics.Direct3D12.D3D12_TRIM_NOTIFICATION_FLAGS.D3D12_TRIM_NOTIFICATION_FLAG_RESTART_PERIODIC_TRIM added -Windows.Win32.Graphics.Direct3D12.D3D12_TRIM_NOTIFICATION_FLAGS.D3D12_TRIM_NOTIFICATION_FLAG_TRIM_TO_BUDGET added -Windows.Win32.Graphics.Direct3D12.D3D12_UAV_DIMENSION.D3D12_UAV_DIMENSION_BUFFER_BYTE_OFFSET added -Windows.Win32.Graphics.Direct3D12.D3D12_UNORDERED_ACCESS_VIEW_DESC._Anonymous_e__Union.BufferByteOffset added -Windows.Win32.Graphics.Direct3D12.ID3D12ApplicationIdentity added -Windows.Win32.Graphics.Direct3D12.ID3D12Device15 added -Windows.Win32.Graphics.Direct3D12.ID3D12DeviceStatistics added -Windows.Win32.Graphics.Direct3D12.ID3D12RuntimeValidationControl added -Windows.Win32.Media.MediaFoundation.D3D12_VIDEO_ENCODER_AV1_FEATURE_FLAGS.D3D12_VIDEO_ENCODER_AV1_FEATURE_FLAG_DISABLE_CDF_UPDATE_UNSUPPORTED added -Windows.Win32.Media.MediaFoundation.D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_HEVC_FLAGS.D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_HEVC_FLAG_STRONG_INTRA_SMOOTHING_ENABLED added -Windows.Win32.Media.MediaFoundation.D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_HEVC_FLAGS.D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_HEVC_FLAG_TEMPORAL_MVP_ENABLED added -Windows.Win32.Media.MediaFoundation.D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_SUPPORT_HEVC_FLAGS1.D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_SUPPORT_HEVC_FLAG1_STRONG_INTRA_SMOOTHING_ENABLED_REQUIRED added -Windows.Win32.Media.MediaFoundation.D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_SUPPORT_HEVC_FLAGS1.D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_SUPPORT_HEVC_FLAG1_STRONG_INTRA_SMOOTHING_ENABLED_SUPPORT added -Windows.Win32.Media.MediaFoundation.D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_SUPPORT_HEVC_FLAGS1.D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_SUPPORT_HEVC_FLAG1_TEMPORAL_MVP_ENABLED_REQUIRED added -Windows.Win32.Media.MediaFoundation.D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_SUPPORT_HEVC_FLAGS1.D3D12_VIDEO_ENCODER_CODEC_CONFIGURATION_SUPPORT_HEVC_FLAG1_TEMPORAL_MVP_ENABLED_SUPPORT added -Windows.Win32.Media.MediaFoundation.D3D12_VIDEO_ENCODER_SUPPORT_FLAGS.D3D12_VIDEO_ENCODER_SUPPORT_FLAG_INTRA_REFRESH_NO_SUBREGION_LAYOUT_CONSTRAINTS_AVAILABLE added -winmd1: Windows.Win32.Graphics.Direct3D12.Apis.D3D12_PREVIEW_SDK_VERSION = 717, winmd2 = 719 -winmd1: Windows.Win32.Graphics.Direct3D12.Apis.D3D12_SDK_VERSION = 618, winmd2 = 619 -winmd1: Windows.Win32.Graphics.Direct3D12.D3D_SHADER_MODEL.D3D_HIGHEST_SHADER_MODEL = 105, winmd2 = 106 -# Upgrade D3D12 Agility SDK to 1.619.1 -Windows.Win32.Graphics.Direct3D12.ID3D12RootSignature1 added -# Upgrade D3D12 Agility SDK to 1.619.3 -Windows.Win32.Graphics.Direct3D12.D3D12_MESSAGE_ID.D3D12_MESSAGE_ID_DEGENERATE_SPLIT_BARRIER added -winmd1: Windows.Win32.Graphics.Direct3D12.D3D12_MESSAGE_ID.D3D12_MESSAGE_ID_D3D12_MESSAGES_END = 1442, winmd2 = 1467 -# Annotate reserved parameters of MsiDatabaseGenerateTransform -Windows.Win32.System.ApplicationInstallationAndServicing.Apis.MsiDatabaseGenerateTransformA : iReserved1 : [In] => [In,Reserved] -Windows.Win32.System.ApplicationInstallationAndServicing.Apis.MsiDatabaseGenerateTransformA : iReserved2 : [In] => [In,Reserved] -Windows.Win32.System.ApplicationInstallationAndServicing.Apis.MsiDatabaseGenerateTransformW : iReserved1 : [In] => [In,Reserved] -Windows.Win32.System.ApplicationInstallationAndServicing.Apis.MsiDatabaseGenerateTransformW : iReserved2 : [In] => [In,Reserved] -# Add IVssBackupComponents and related VSS backup interfaces from vsbackup.h (fixes #2095) -Windows.Win32.Storage.Vss.Apis.CreateVssBackupComponentsInternal added -Windows.Win32.Storage.Vss.Apis.CreateVssExamineWriterMetadataInternal added -Windows.Win32.Storage.Vss.Apis.GetProviderMgmtInterfaceInternal added -Windows.Win32.Storage.Vss.Apis.IsVolumeSnapshottedInternal added -Windows.Win32.Storage.Vss.Apis.ShouldBlockRevertInternal added -Windows.Win32.Storage.Vss.Apis.VSS_SW_BOOTABLE_STATE added -Windows.Win32.Storage.Vss.Apis.VssFreeSnapshotPropertiesInternal added -Windows.Win32.Storage.Vss.IVssBackupComponents added -Windows.Win32.Storage.Vss.IVssBackupComponentsEx added -Windows.Win32.Storage.Vss.IVssBackupComponentsEx2 added -Windows.Win32.Storage.Vss.IVssBackupComponentsEx3 added -Windows.Win32.Storage.Vss.IVssBackupComponentsEx4 added -Windows.Win32.Storage.Vss.IVssExamineWriterMetadata added -Windows.Win32.Storage.Vss.IVssExamineWriterMetadataEx added -Windows.Win32.Storage.Vss.IVssExamineWriterMetadataEx2 added -Windows.Win32.Storage.Vss.IVssWMComponent added -Windows.Win32.Storage.Vss.IVssWriterComponentsExt added -Windows.Win32.Storage.Vss.VSS_COMPONENTINFO added diff --git a/scripts/CommonUtils.ps1 b/scripts/CommonUtils.ps1 index 433773887..a9deeddbe 100644 --- a/scripts/CommonUtils.ps1 +++ b/scripts/CommonUtils.ps1 @@ -112,11 +112,6 @@ function Get-LibMappingsFile return $libMappingOutputFileName } -function Get-ChangesSinceLastReleaseFile -{ - return Join-Path $PSScriptRoot "ChangesSinceLastRelease.txt" -} - function Get-NugetPropsProperty { Param ([string] $name, [string]$projectName) diff --git a/scripts/CompareBinToLastRelease.ps1 b/scripts/CompareBinToLastRelease.ps1 index 54f625095..30dc3a433 100644 --- a/scripts/CompareBinToLastRelease.ps1 +++ b/scripts/CompareBinToLastRelease.ps1 @@ -1,10 +1,7 @@ [CmdletBinding()] param ( [switch] - $SkipInstallTools, - - [string] - $UpdateDifferencesWithComment + $SkipInstallTools ) . "$PSScriptRoot\CommonUtils.ps1" @@ -17,25 +14,15 @@ if (!$SkipInstallTools.IsPresent) $winmdPath = Get-OutputWinmdFileName -arch "crossarch" $previousReleaseWinmd = Get-Win32MetadataLastReleaseWinmdPath $winmdUtilsPathBin = Join-Path $metadataToolsBin "WinmdUtils.dll" -$changesSinceLastRelease = Get-ChangesSinceLastReleaseFile Write-Verbose "Comparing $winmdPath to previous release $previousReleaseWinmd..." -$utilsArgs = @('compare', '--first', $previousReleaseWinmd, '--second', $winmdPath, '--knownDiffsFile', $changesSinceLastRelease) -if ($UpdateDifferencesWithComment) -{ - $utilsArgs += '--updateKnownDiffsComment', $UpdateDifferencesWithComment -} +$utilsArgs = @('compare', '--first', $previousReleaseWinmd, '--second', $winmdPath) Write-Verbose "Calling: dotnet $utilsArgs" & dotnet $winmdUtilsPathBin $utilsArgs if ($LastExitCode -lt 0) { - if (!$SuppressSuggestionToCallScript.IsPresent) - { - Write-Error "If all the differences are expected, please update the expected differences list:`n.\scripts\UpdateChangesSinceLastRelease.ps1 ''" - } - exit -1 } diff --git a/scripts/DiffWinmdToBaseline.ps1 b/scripts/DiffWinmdToBaseline.ps1 new file mode 100644 index 000000000..8b757851f --- /dev/null +++ b/scripts/DiffWinmdToBaseline.ps1 @@ -0,0 +1,85 @@ +<# +.SYNOPSIS + Shows a unified diff of the current build's winmd API surface against the last release. + +.DESCRIPTION + Uses WinmdUtils to decompile both the current build's winmd and the last released + winmd to sorted C# declarations, then displays a unified diff of the changes. + + This is a local development helper for reviewing API changes before submitting a PR. + The same diff is automatically posted as a PR comment by the CI pipeline. + +.PARAMETER SkipInstallTools + Skip building the build tools (assumes they are already built). + +.EXAMPLE + .\scripts\DiffWinmdToBaseline.ps1 + # Shows the full API diff against the last release + +.EXAMPLE + .\scripts\DiffWinmdToBaseline.ps1 -SkipInstallTools | Out-File diff.patch + # Save the diff to a file for review +#> +[CmdletBinding()] +param ( + [switch]$SkipInstallTools +) + +. "$PSScriptRoot\CommonUtils.ps1" + +if (!$SkipInstallTools.IsPresent) +{ + Install-BuildTools | Write-Host +} + +$winmdPath = Get-OutputWinmdFileName -arch "crossarch" +$previousReleaseWinmd = Get-Win32MetadataLastReleaseWinmdPath +$winmdUtilsPathBin = Join-Path $metadataToolsBin "WinmdUtils.dll" + +if (!(Test-Path $winmdPath)) +{ + Write-Error "Current build winmd not found at $winmdPath. Run a build first." + exit 1 +} + +if (!(Test-Path $previousReleaseWinmd)) +{ + Write-Error "Previous release winmd not found at $previousReleaseWinmd. Run 'dotnet restore buildtools' first." + exit 1 +} + +$tempDir = Join-Path ([System.IO.Path]::GetTempPath()) "win32metadata-diff" +New-Item -ItemType Directory -Force -Path $tempDir | Out-Null + +$baselineDump = Join-Path $tempDir "baseline.cs" +$currentDump = Join-Path $tempDir "current.cs" + +Write-Host "Dumping baseline winmd..." -ForegroundColor Blue +& dotnet $winmdUtilsPathBin dump --winmd $previousReleaseWinmd --output $baselineDump +if ($LastExitCode -ne 0) { Write-Error "Failed to dump baseline winmd"; exit 1 } + +Write-Host "Dumping current winmd..." -ForegroundColor Blue +& dotnet $winmdUtilsPathBin dump --winmd $winmdPath --output $currentDump +if ($LastExitCode -ne 0) { Write-Error "Failed to dump current winmd"; exit 1 } + +Write-Host "`nAPI Diff (baseline -> current):" -ForegroundColor Green +Write-Host "═══════════════════════════════════════════════════════════════" -ForegroundColor Green + +# Use git diff for nice colored unified diff output +& git diff --no-index --stat $baselineDump $currentDump +Write-Host "" +& git diff --no-index $baselineDump $currentDump + +$exitCode = $LastExitCode + +# Clean up +Remove-Item $tempDir -Recurse -Force -ErrorAction SilentlyContinue + +if ($exitCode -eq 0) +{ + Write-Host "`nNo API differences found." -ForegroundColor Green +} +else +{ + Write-Host "`nDifferences shown above." -ForegroundColor Yellow +} diff --git a/scripts/Set-LastReleaseVersion.ps1 b/scripts/Set-LastReleaseVersion.ps1 index 08d79556c..c6d860b08 100644 --- a/scripts/Set-LastReleaseVersion.ps1 +++ b/scripts/Set-LastReleaseVersion.ps1 @@ -15,9 +15,3 @@ $dirBuildPropsXml.Load($dirBuildPropsFile) $dirBuildPropsXml.Project.PropertyGroup.LastWin32MetadataReleaseVersion = $LastReleaseVersion $dirBuildPropsXml.Save($dirBuildPropsFile) -. "$PSScriptRoot\CommonUtils.ps1" -$changesFile = Get-ChangesSinceLastReleaseFile -Write-Verbose "Clearing contents of $changesFile..." - -Clear-Content -Path $changesFile - diff --git a/scripts/TestWinmdBinary.ps1 b/scripts/TestWinmdBinary.ps1 index 6c63a3400..6bfb084cf 100644 --- a/scripts/TestWinmdBinary.ps1 +++ b/scripts/TestWinmdBinary.ps1 @@ -19,15 +19,19 @@ ThrowOnNativeProcessError Write-Host "`n`e[32mTesting .winmd succeeded`e[0m" -Write-Host "*** Comparing .winmd to last release" -ForegroundColor Blue +Write-Host "*** Comparing .winmd to last release (informational)" -ForegroundColor Blue +# Run the comparison for informational purposes — differences are expected +# and will be reviewed via the PR diff comment posted by CI. & "$PSScriptRoot\CompareBinToLastRelease.ps1" -SkipInstallTools if ($LastExitCode -lt 0) { - exit -1 + Write-Host "`e[33mAPI differences detected (see above). These will be posted as a PR comment for review.`e[0m" +} +else +{ + Write-Host "`n`e[32mNo API differences from last release.`e[0m" } - -Write-Host "`n`e[32mComparing .winmd to last release succeeded`e[0m" exit 0 diff --git a/scripts/UpdateChangesSinceLastRelease.ps1 b/scripts/UpdateChangesSinceLastRelease.ps1 deleted file mode 100644 index ee579e02a..000000000 --- a/scripts/UpdateChangesSinceLastRelease.ps1 +++ /dev/null @@ -1,9 +0,0 @@ -param -( - [string] - $ReasonForAddedDifferences -) - -. "$PSScriptRoot\CommonUtils.ps1" - -& "$PSScriptRoot\CompareBinToLastRelease.ps1" -SkipInstallTools -UpdateDifferencesWithComment $ReasonForAddedDifferences diff --git a/sources/WinmdUtils/Program.cs b/sources/WinmdUtils/Program.cs index 1bd61b342..0b93faf2e 100644 --- a/sources/WinmdUtils/Program.cs +++ b/sources/WinmdUtils/Program.cs @@ -12,6 +12,7 @@ using System.Text; using System.Text.RegularExpressions; using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.CSharp; using ICSharpCode.Decompiler.Metadata; using ICSharpCode.Decompiler.TypeSystem; using MetadataUtils; @@ -134,6 +135,14 @@ static int Main(string[] args) showBrokenArchTypes.Handler = CommandHandler.Create(ShowBrokenArchTypes); + var dumpCommand = new Command("dump", "Dump all types in a winmd as sorted C# declarations.") + { + new Option("--winmd", "The winmd to dump.") { IsRequired = true }.ExistingOnly(), + new Option("--output", "Output file path. If not specified, writes to stdout.") { IsRequired = false }, + }; + + dumpCommand.Handler = CommandHandler.Create(DumpWinmd); + var rootCommand = new RootCommand("Win32metadata winmd utils") { showMissingImportsCommand, @@ -144,6 +153,7 @@ static int Main(string[] args) showPointersToDelegates, showSuggestedRemappings, compareCommand, + dumpCommand, showLibImports, createLibRsp, showNamespaceDependencies, @@ -1846,6 +1856,107 @@ public static int CompareWinmds(FileInfo first, FileInfo second, FileInfo knownD } } + public static int DumpWinmd(FileInfo winmd, FileInfo output, IConsole console) + { + var settings = new DecompilerSettings() + { + ThrowOnAssemblyResolveErrors = false, + LoadInMemory = true + }; + + var decompiler = new CSharpDecompiler(winmd.FullName, settings); + + // Map types from metadata, sorted by full name for deterministic output + var peFile = decompiler.TypeSystem.MainModule.PEFile; + var metadata = peFile.Metadata; + + var sortedHandles = new List(); + var typeFullNames = new Dictionary(); + + foreach (var handle in metadata.TypeDefinitions) + { + var typeDef = metadata.GetTypeDefinition(handle); + string ns = metadata.GetString(typeDef.Namespace); + string name = metadata.GetString(typeDef.Name); + string fullName = string.IsNullOrEmpty(ns) ? name : $"{ns}.{name}"; + + if (fullName == "") + continue; + + // Skip nested types — they'll be decompiled as part of their parent + if (typeDef.GetDeclaringType().IsNil == false) + continue; + + sortedHandles.Add(handle); + typeFullNames[handle] = fullName; + } + + sortedHandles.Sort((a, b) => string.Compare(typeFullNames[a], typeFullNames[b], StringComparison.Ordinal)); + + TextWriter writer; + if (output != null) + { + writer = new StreamWriter(output.FullName, false, Encoding.UTF8); + } + else + { + writer = Console.Out; + } + + try + { + writer.WriteLine("// Win32 Metadata API Surface"); + writer.WriteLine($"// Source: {winmd.Name}"); + writer.WriteLine(); + + string currentNamespace = null; + foreach (var handle in sortedHandles) + { + string fullName = typeFullNames[handle]; + string ns = fullName.Contains('.') ? fullName.Substring(0, fullName.LastIndexOf('.')) : ""; + + if (ns != currentNamespace) + { + if (currentNamespace != null) + { + writer.WriteLine(); + } + writer.WriteLine($"// ═══════════════════════════════════════════════════════════════"); + writer.WriteLine($"// Namespace: {ns}"); + writer.WriteLine($"// ═══════════════════════════════════════════════════════════════"); + writer.WriteLine(); + currentNamespace = ns; + } + + try + { + var fullTypeName = new ICSharpCode.Decompiler.TypeSystem.FullTypeName(fullName); + var syntaxTree = decompiler.DecompileType(fullTypeName); + writer.WriteLine(syntaxTree.ToString()); + } + catch (Exception ex) + { + writer.WriteLine($"// ERROR decompiling {fullName}: {ex.Message}"); + writer.WriteLine(); + } + } + + if (output != null) + { + console.Out.WriteLine($"Dumped {sortedHandles.Count} types to {output.FullName}"); + } + } + finally + { + if (output != null) + { + writer.Dispose(); + } + } + + return 0; + } + public static int ShowMissingImports(FileInfo first, FileInfo second, string exclusions, IConsole console) { int ret = 0; diff --git a/sources/WinmdUtils/Properties/launchSettings.json b/sources/WinmdUtils/Properties/launchSettings.json index bf7c1b749..8fb8962a2 100644 --- a/sources/WinmdUtils/Properties/launchSettings.json +++ b/sources/WinmdUtils/Properties/launchSettings.json @@ -6,11 +6,11 @@ }, "compare": { "commandName": "Project", - "commandLineArgs": "compare --first $(USERPROFILE)\\.nuget\\packages\\microsoft.windows.sdk.win32metadata\\49.0.21-preview\\Windows.Win32.winmd --second $(ProjectDir)..\\..\\bin\\Windows.Win32.winmd --knownDiffsFile $(ProjectDir)..\\..\\scripts\\ChangesSinceLastRelease.txt" + "commandLineArgs": "compare --first $(USERPROFILE)\\.nuget\\packages\\microsoft.windows.sdk.win32metadata\\49.0.21-preview\\Windows.Win32.winmd --second $(ProjectDir)..\\..\\bin\\Windows.Win32.winmd" }, - "compareWithUpdate": { + "dump": { "commandName": "Project", - "commandLineArgs": "compare --first $(USERPROFILE)\\.nuget\\packages\\microsoft.windows.sdk.win32metadata\\29.0.6-preview\\Windows.Win32.winmd --second $(ProjectDir)..\\..\\bin\\Windows.Win32.winmd --knownDiffsFile $(ProjectDir)..\\..\\scripts\\ChangesSinceLastRelease.txt --updateKnownDiffsComment \"Fix me!\"" + "commandLineArgs": "dump --winmd $(ProjectDir)..\\..\\bin\\Windows.Win32.winmd --output $(ProjectDir)..\\..\\bin\\Windows.Win32.apidump.cs" }, "showDuplicateImports": { "commandName": "Project", From a2c1baf9a71e5037072134dd6e0426cd0de53fd7 Mon Sep 17 00:00:00 2001 From: Jevan Saks Date: Thu, 25 Jun 2026 08:49:37 -0700 Subject: [PATCH 2/3] Diff against main branch instead of last NuGet release The API diff now compares the PR's winmd against main's latest build artifact, showing only the changes introduced by the PR rather than all changes since the last release. Falls back to the NuGet release baseline if no main artifact is available (e.g., first run after enabling this feature). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pr-validation.yml | 34 +++++++++++++++++++++-------- scripts/DiffWinmdToBaseline.ps1 | 3 ++- 2 files changed, 27 insertions(+), 10 deletions(-) diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 7cbf48d4c..7b1fec3b2 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -139,18 +139,36 @@ jobs: shell: pwsh run: .\scripts\DoTests.ps1 - - name: Generate API surface dumps + - name: Download baseline winmd from main + if: github.event_name == 'pull_request' + id: baseline + uses: dawidd6/action-download-artifact@v6 + with: + workflow: pr-validation.yml + branch: ${{ github.event.pull_request.base.ref }} + name: winmd + path: bin/baseline-artifact + search_artifacts: true + if_no_artifact_found: warn + + - 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" - # Get the baseline winmd from the last release NuGet package - . .\scripts\CommonUtils.ps1 - $ErrorActionPreference = 'Continue' - $PSNativeCommandUseErrorActionPreference = $false - $baselineWinmd = Get-Win32MetadataLastReleaseWinmdPath + # 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" @@ -166,7 +184,6 @@ jobs: if ($LASTEXITCODE -ne 0) { throw "Failed to dump current" } # Generate diff — git diff exits 1 when differences exist, which is expected - # Use cmd /c to isolate from PowerShell's native command error handling & cmd /c "git diff --no-index --unified=3 bin\baseline.apidump.cs bin\current.apidump.cs > bin\api-diff.patch 2>&1" $diffExitCode = $LASTEXITCODE @@ -196,7 +213,6 @@ jobs: } # Reset LASTEXITCODE so GitHub Actions doesn't treat this step as failed - # (git diff exits 1 when differences exist, which is expected/success for us) $global:LASTEXITCODE = 0 - name: Post API diff comment on PR @@ -215,7 +231,7 @@ jobs: const body = `${marker} ## šŸ“‹ API Surface Diff - **+${additions} additions / -${deletions} deletions** vs last release ([full build log](${runUrl})) + **+${additions} additions / -${deletions} deletions** vs main branch ([full build log](${runUrl}))
Click to expand API diff diff --git a/scripts/DiffWinmdToBaseline.ps1 b/scripts/DiffWinmdToBaseline.ps1 index 8b757851f..072ebc1e8 100644 --- a/scripts/DiffWinmdToBaseline.ps1 +++ b/scripts/DiffWinmdToBaseline.ps1 @@ -7,7 +7,8 @@ winmd to sorted C# declarations, then displays a unified diff of the changes. This is a local development helper for reviewing API changes before submitting a PR. - The same diff is automatically posted as a PR comment by the CI pipeline. + The CI pipeline automatically diffs against the target branch (main) and posts + a PR comment showing only the changes introduced by the PR. .PARAMETER SkipInstallTools Skip building the build tools (assumes they are already built). From 3ea891915f5a5f2bd9496ccc58c629879332c4aa Mon Sep 17 00:00:00 2001 From: Jevan Saks Date: Thu, 25 Jun 2026 09:08:37 -0700 Subject: [PATCH 3/3] Replace third-party action with actions/github-script for artifact download The microsoft org restricts actions to enterprise-owned or actions/* only. Use actions/github-script to call the GitHub API directly to download the baseline winmd artifact from the target branch's latest successful run. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/pr-validation.yml | 64 +++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 7b1fec3b2..2ad6bf76a 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -139,17 +139,65 @@ jobs: shell: pwsh run: .\scripts\DoTests.ps1 - - name: Download baseline winmd from main + - name: Download baseline winmd from target branch if: github.event_name == 'pull_request' id: baseline - uses: dawidd6/action-download-artifact@v6 + uses: actions/github-script@v7 with: - workflow: pr-validation.yml - branch: ${{ github.event.pull_request.base.ref }} - name: winmd - path: bin/baseline-artifact - search_artifacts: true - if_no_artifact_found: warn + 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'