Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
228 changes: 228 additions & 0 deletions .github/workflows/pr-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -135,6 +139,230 @@ jobs:
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()
Expand Down
20 changes: 15 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,20 +252,30 @@ 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
* APIs were moved to different namespaces

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.
Expand Down
2 changes: 1 addition & 1 deletion docs/copilot/research/win32metadata-detailed-research.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/`.

Expand Down
2 changes: 1 addition & 1 deletion docs/copilot/research/win32metadata-research-summary.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading
Loading