Skip to content

Commit 4fd6fbb

Browse files
[main] Source code updates from dotnet/fsharp (#10097)
Co-authored-by: dotnet-maestro[bot] <dotnet-maestro[bot]@users.noreply.github.com> Co-authored-by: Tomas Grosup <Tomas.Grosup@gmail.com>
1 parent 907535d commit 4fd6fbb

85 files changed

Lines changed: 3596 additions & 1149 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
applyTo:
3+
- "Directory.Packages.props"
4+
- "eng/Packages.props"
5+
- "eng/Versions.props"
6+
- "eng/Version.Details.xml"
7+
- "eng/Version.Details.props"
8+
- "eng/Signing.props"
9+
- "src/**/*.{fsproj,props,targets}"
10+
---
11+
12+
# Shipping binary dependencies
13+
14+
A package change is also a product-layout and signing change when any assembly from the package reaches a shipped archive, NuGet package, tool, or SDK layout. This includes new transitive assemblies introduced by a version update.
15+
16+
Before merging such a change:
17+
18+
- Enumerate the complete new DLL closure in every shipped layout, not only the directly referenced package. Inspect the publish/package output or the MSBuild items that compose it.
19+
- Identify every repository that performs a later packaging or signing pass. A component's `eng/Signing.props` applies to that component's build; it does not automatically configure a downstream owner that rebundles the files. For example, FSI assemblies copied into the .NET SDK are signed again under the SDK's signing policy.
20+
- Classify each new DLL at every signing boundary: Microsoft-owned, third-party re-signed with the repository's external certificate, or intentionally left unchanged. Do not assume an upstream signature or `FileSignInfo` survives a downstream container-signing pass.
21+
- Keep product behavior and dependency declarations in dotnet/fsharp. If the integrated VMR build needs an additional final-layout policy entry owned by another component, make the smallest explicit integration change and request review from that component's owners; do not distort F# product files to satisfy a downstream packager.
22+
- Validate both the F# build and the relevant VMR legs. A green dotnet/fsharp build does not exercise the SDK's Windows dry-run container signing. Treat `SIGN004` in a downstream layout as evidence that the final signing owner is missing or misclassifying a shipped file.
23+
- When the same source commit flows to multiple aligned VMR branches, compare the generated product trees. Resolve F# product code, tests, baselines, and release notes from dotnet/fsharp; preserve genuinely VMR-owned infrastructure such as synchronized `eng/common` or final-layout policy.
24+
25+
For codeflow conflicts, run the exact `darc vmr resolve-conflict --subscription ...` command from the bot before resolving files. Review the staged result rather than reconstructing the flow by hand: crossing flows can duplicate release-note entries, and moved definitions can make an apparently necessary conflict side a duplicate.
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
$ErrorActionPreference = 'Stop'
2+
$root = Join-Path $PSScriptRoot ('.pr-validation-tests-' + [Guid]::NewGuid())
3+
$count = 0
4+
New-Item -ItemType Directory $root | Out-Null
5+
6+
function Assert($Condition, $Message) {
7+
if (!$Condition) { throw $Message }
8+
}
9+
10+
function Assert-Snapshot($Request) {
11+
Assert ($Request.resources.repositories.self.refName -ceq 'refs/pull/42/head') 'Wrong YAML ref'
12+
Assert ($Request.resources.repositories.self.version -ceq ('a' * 40)) 'Wrong YAML revision'
13+
Assert ($Request.templateParameters.prNumber -is [string] -and $Request.templateParameters.prNumber -ceq '42') 'PR number must be a string'
14+
Assert ($Request.templateParameters.headSha -ceq ('a' * 40)) 'Wrong head snapshot'
15+
Assert ($Request.templateParameters.baseSha -ceq ('b' * 40)) 'Wrong base snapshot'
16+
}
17+
18+
function Invoke-WebRequest {
19+
[CmdletBinding()]
20+
param($Uri, $Method, $Headers, $UserAgent, $ContentType, $MaximumRedirection, [switch]$SkipHttpErrorCheck, $Body)
21+
$calls.Add($PSBoundParameters)
22+
Assert ($responses.ContainsKey($Uri)) "Unexpected URL: $Uri"
23+
Assert ($Headers.Authorization -ceq "Bearer $expectedToken") 'Wrong bearer token'
24+
Assert ($Headers.Accept -ceq 'application/json' -and $ContentType -ceq 'application/json') 'Wrong JSON headers'
25+
Assert ($UserAgent -ceq 'fsharp-pr-validation') 'Missing user agent'
26+
Assert ($MaximumRedirection -ceq 0 -and $SkipHttpErrorCheck -and $PSBoundParameters.ErrorAction -eq 'Stop') 'HTTP handling is not fail-closed'
27+
$response = $responses[$Uri]
28+
Assert ($Method -ceq $response.Method) 'Wrong HTTP method'
29+
if ($response.Failure) { throw 'Network failed (test-token)' }
30+
@{
31+
StatusCode = $response.Status
32+
Content = if ($response.ContainsKey('Raw')) { $response.Raw } else { ConvertTo-Json -InputObject $response.Body -Depth 10 -Compress }
33+
}
34+
}
35+
36+
function Test-Case($Name, [scriptblock]$Change = {}, $ExpectedError = '', $Mode = 'Prepare', $CallCount = -1) {
37+
$event = @{
38+
comment = @{ body = "/dart $('a' * 40)"; user = @{ login = 'maintainer'; type = 'User' } }
39+
issue = @{ number = 42; pull_request = @{} }
40+
}
41+
$pr = @{
42+
state = 'open'
43+
head = @{ sha = 'a' * 40; repo = @{ full_name = 'contributor/fsharp' } }
44+
base = @{ sha = 'b' * 40; ref = 'main'; repo = @{ full_name = 'dotnet/fsharp' } }
45+
}
46+
$permission = @{ permission = 'write' }
47+
$request = @{
48+
resources = @{ repositories = @{ self = @{ refName = 'refs/pull/42/head'; version = 'a' * 40 } } }
49+
templateParameters = @{ prNumber = '42'; headSha = 'a' * 40; baseSha = 'b' * 40 }
50+
}
51+
$permissionUri = 'https://api.github.com/repos/dotnet/fsharp/collaborators/maintainer/permission'
52+
$pullUri = 'https://api.github.com/repos/dotnet/fsharp/pulls/42'
53+
$memberUri = 'https://api.github.com/orgs/microsoft/members/maintainer'
54+
$queueUri = 'https://dev.azure.com/devdiv/DevDiv/_apis/pipelines/123/runs?api-version=7.1'
55+
$reportUri = 'https://api.github.com/repos/dotnet/fsharp/issues/42/comments'
56+
$responses = @{
57+
$permissionUri = @{ Method = 'GET'; Status = 200; Body = $permission }
58+
$pullUri = @{ Method = 'GET'; Status = 200; Body = $pr }
59+
$memberUri = @{ Method = 'GET'; Status = 204 }
60+
$queueUri = @{ Method = 'POST'; Status = 200; Body = @{ id = 456 } }
61+
$reportUri = @{ Method = 'POST'; Status = 201; Body = @{ id = 789 } }
62+
}
63+
$calls = [Collections.Generic.List[object]]::new()
64+
$env:GITHUB_EVENT_PATH = Join-Path $root 'event.json'
65+
$env:GITHUB_OUTPUT = Join-Path $root 'output'
66+
$env:GITHUB_REPOSITORY = 'dotnet/fsharp'; $env:GITHUB_ACTOR = 'rerunning-admin'
67+
$env:GITHUB_SERVER_URL = 'https://github.com'; $env:GITHUB_RUN_ID = '987'
68+
$env:GITHUB_TOKEN = if ($Mode -eq 'Membership') { 'membership-token' } else { 'github-token' }
69+
$env:PIPELINE_ID = '123'; $env:AZDO_TOKEN = 'test-token'
70+
$env:REQUEST_BODY = ConvertTo-Json -InputObject $request -Depth 10 -Compress
71+
$env:RUN_URL = ''; $env:HEAD_SHA = 'a' * 40; $env:BASE_SHA = 'b' * 40
72+
$expectedToken = if ($Mode -eq 'Queue') { $env:AZDO_TOKEN } else { $env:GITHUB_TOKEN }
73+
. $Change
74+
ConvertTo-Json -InputObject $event -Depth 10 | Set-Content -LiteralPath $env:GITHUB_EVENT_PATH -Encoding utf8
75+
Remove-Item -LiteralPath $env:GITHUB_OUTPUT -ErrorAction SilentlyContinue
76+
$failure = $null
77+
try { $stdout = @(& "$PSScriptRoot\pr-validation.ps1" -Mode $Mode) }
78+
catch { $failure = "$_" }
79+
if ($ExpectedError) {
80+
Assert ($failure -and $failure -match $ExpectedError) "$Name failed for wrong reason: $failure"
81+
Assert (!(Test-Path -LiteralPath $env:GITHUB_OUTPUT)) "$Name wrote outputs on failure"
82+
Assert ($failure -notmatch 'test-token|github-token|membership-token') 'Leaked token'
83+
}
84+
else {
85+
Assert (!$failure) "$Name failed: $failure"
86+
Assert ($stdout.Count -eq 0) "$Name leaked stdout"
87+
switch ($Mode) {
88+
'Prepare' {
89+
$lines = @(Get-Content -LiteralPath $env:GITHUB_OUTPUT)
90+
Assert ($lines.Count -eq 3 -and $lines[1] -ceq "head=$('a' * 40)" -and $lines[2] -ceq "base=$('b' * 40)") 'Wrong snapshot outputs'
91+
Assert-Snapshot (ConvertFrom-Json $lines[0].Substring(5))
92+
Assert ($calls[0].Uri -ceq $permissionUri -and $calls[1].Uri -ceq $pullUri) 'Wrong requester or PR'
93+
}
94+
'Queue' {
95+
Assert-Snapshot (ConvertFrom-Json $calls[0].Body)
96+
Assert ((Get-Content -LiteralPath $env:GITHUB_OUTPUT -Raw).Trim() -ceq 'url=https://dev.azure.com/devdiv/DevDiv/_build/results?buildId=456') 'Wrong run link'
97+
}
98+
'Report' {
99+
$body = (ConvertFrom-Json $calls[0].Body).body
100+
$expected = if ($env:RUN_URL) { '[F# Apex run](https://example.invalid/run) requested by @maintainer.' }
101+
else { 'F# Apex request failed. [Workflow details](https://github.com/dotnet/fsharp/actions/runs/987).' }
102+
Assert ($body.StartsWith($expected)) 'Wrong PR feedback'
103+
Assert ($body.Contains('`/dart <sha>`') -and $body.Contains('`/pr-val <sha>`')) 'Missing retry guidance'
104+
if ($env:RUN_URL) { Assert ($body.Contains("Reviewed PR head and pipeline YAML: ``$env:HEAD_SHA``") -and $body.Contains("Base: ``$env:BASE_SHA``")) 'Missing snapshot identifiers' }
105+
}
106+
}
107+
if ($CallCount -lt 0) { $CallCount = if ($Mode -eq 'Prepare') { 2 } else { 1 } }
108+
}
109+
if ($CallCount -ge 0) { Assert ($calls.Count -eq $CallCount) "$Name made unexpected HTTP calls" }
110+
$script:count++
111+
Write-Host "PASS: $Name"
112+
}
113+
114+
try {
115+
foreach ($command in "/dart $('a' * 40)", "/pr-val $('a' * 40)", " `n/pr-val $('a' * 40) `n") {
116+
foreach ($authorRepo in 'dotnet/fsharp', 'contributor/fsharp') {
117+
Test-Case "$command from $authorRepo" { $event.comment.body = $command; $pr.head.repo.full_name = $authorRepo }
118+
}
119+
}
120+
foreach ($command in '/dart', '/pr-val', '/dart abc1234', 'please /dart', '`/dart`', "/dart`n/pr-val", '/dart-extra', '/DART', "/dart`nextra", "/dart $('A' * 40)", "/dart $('a' * 39)", "/dart $('a' * 41)", "/dart $('g' * 40)", "/dart $('a' * 40) extra", "/dart`n$('a' * 40)", "/dart $('a' * 40)", '', $null, 42) {
121+
Test-Case "reject command '$command'" { $event.comment.body = $command } 'standalone' -CallCount 0
122+
}
123+
foreach ($alias in 'dart', 'pr-val') {
124+
Test-Case "$alias rejects base SHA" { $event.comment.body = "/$alias $('b' * 40)" } 'current PR head' -CallCount 2
125+
Test-Case "$alias rejects stale reviewed SHA" { $event.comment.body = "/$alias $('c' * 40)" } 'current PR head' -CallCount 2
126+
Test-Case "$alias rejects updated PR head" { $event.comment.body = "/$alias $('a' * 40)"; $pr.head.sha = 'c' * 40 } 'current PR head' -CallCount 2
127+
}
128+
foreach ($access in 'write', 'maintain', 'admin') { Test-Case "permission $access" { $permission.permission = $access } }
129+
foreach ($access in 'read', 'triage', 'WRITE', '', $null, @('write')) { Test-Case "reject permission '$access'" { $permission.permission = $access } 'write access' -CallCount 1 }
130+
Test-Case 'bot' { $event.comment.user.type = 'Bot' } 'user comments' -CallCount 0
131+
Test-Case 'array user type' { $event.comment.user.type = @('User') } 'user comments' -CallCount 0
132+
Test-Case 'issue' { $event.issue.Remove('pull_request') } 'dotnet/fsharp PRs' -CallCount 0
133+
Test-Case 'false PR marker' { $event.issue.pull_request = $false } 'dotnet/fsharp PRs' -CallCount 0
134+
Test-Case 'array event' { $event = @($event) } 'dotnet/fsharp PRs' -CallCount 0
135+
Test-Case 'array permission response' { $responses[$permissionUri].Body = @($permission) } 'write access'
136+
Test-Case 'array PR response' { $responses[$pullUri].Body = @($pr) } 'open PRs'
137+
foreach ($repo in 'other/fsharp', 'dotnet/other', 'DotNet/fsharp') {
138+
Test-Case "wrong repo $repo" { $env:GITHUB_REPOSITORY = $repo } 'dotnet/fsharp PRs' -CallCount 0
139+
}
140+
foreach ($id in '1; command', '42', $null, 0, -1, 1.5, $true, 9007199254740992) {
141+
Test-Case "bad PR number '$id'" { $event.issue.number = $id } 'PR number' -CallCount 0
142+
}
143+
Test-Case 'closed PR' { $pr.state = 'closed' } 'open PRs'
144+
Test-Case 'uppercase state' { $pr.state = 'OPEN' } 'open PRs'
145+
Test-Case 'array state' { $pr.state = @('open') } 'open PRs'
146+
Test-Case 'array target' { $pr.base.repo.full_name = @('dotnet/fsharp') } 'main'
147+
Test-Case 'array branch' { $pr.base.ref = @('main') } 'main'
148+
foreach ($branch in 'release/test', 'Main') { Test-Case "non-main $branch" { $pr.base.ref = $branch } 'main' }
149+
Test-Case 'foreign target' { $pr.base.repo.full_name = 'other/fsharp' } 'main'
150+
foreach ($side in 'head', 'base') {
151+
foreach ($sha in 'abcdef0', $null, ('A' * 40), (('a' * 40) + "`n"), 123, @('a' * 40)) {
152+
Test-Case "bad $side SHA '$sha'" { $pr[$side].sha = $sha } 'full head/base'
153+
}
154+
}
155+
Test-Case 'membership original commenter' -Mode Membership
156+
foreach ($status in 200, 201, 301, 302, 307, 308, 401, 403, 404, 429, 500) {
157+
Test-Case "membership HTTP $status" { $responses[$memberUri].Status = $status } "HTTP $status" Membership
158+
}
159+
foreach ($endpoint in 'permissionUri', 'pullUri', 'memberUri', 'queueUri', 'reportUri') {
160+
$mode = switch ($endpoint) { memberUri { 'Membership' }; queueUri { 'Queue' }; reportUri { 'Report' }; default { 'Prepare' } }
161+
Test-Case "$endpoint network failure" { $responses[(Get-Variable $endpoint -ValueOnly)].Failure = $true } 'HTTP request failed' $mode
162+
Test-Case "$endpoint API failure" { $responses[(Get-Variable $endpoint -ValueOnly)].Status = 500 } 'HTTP 500' $mode
163+
}
164+
Test-Case 'queue snapshot' -Mode Queue
165+
Test-Case 'queue created' { $responses[$queueUri].Status = 201 } -Mode Queue
166+
foreach ($json in '{"id":456.0}', '{"id":4.56e2}') {
167+
Test-Case "integral JSON number $json" { $responses[$queueUri].Raw = $json } -Mode Queue
168+
}
169+
foreach ($pipeline in '', '0', '123/path', "123`n", ' 123', '+123') {
170+
Test-Case "invalid pipeline '$pipeline'" { $env:PIPELINE_ID = $pipeline } 'Configure FSHARP_APEX_PIPELINE_ID' Queue 0
171+
}
172+
foreach ($token in '', ' ') { Test-Case 'missing queue token' { $env:AZDO_TOKEN = $token } 'access token' Queue 0 }
173+
Test-Case 'invalid request JSON' { $env:REQUEST_BODY = '{' } 'JSON' Queue 0
174+
foreach ($status in 301, 302, 303, 307, 308, 401, 403, 429, 500) {
175+
Test-Case "queue HTTP $status" { $responses[$queueUri].Status = $status } "HTTP $status" Queue
176+
}
177+
foreach ($id in $null, 0, -1, 1.5, '456', $true, 9007199254740992, @(456)) {
178+
Test-Case "invalid run ID '$id'" { $responses[$queueUri].Body.id = $id } 'valid run ID' Queue
179+
}
180+
foreach ($json in '{}', 'null', '[{"id":456}]', '{"id":[456]}', '{"id":{}}', '{"ID":456}') {
181+
Test-Case "invalid run $json" { $responses[$queueUri].Raw = $json } 'valid run ID' Queue
182+
}
183+
Test-Case 'invalid response JSON' { $responses[$queueUri].Raw = '<html>' } 'JSON' Queue
184+
Test-Case 'report success' { $env:RUN_URL = 'https://example.invalid/run' } -Mode Report
185+
Test-Case 'report failure' -Mode Report
186+
Write-Host "$count tests passed."
187+
}
188+
finally { Remove-Item -LiteralPath $root -Recurse -Force }

0 commit comments

Comments
 (0)