Skip to content
Open
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
5 changes: 3 additions & 2 deletions .azure-pipelines/hidi-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,11 +165,12 @@ extends:
MaxRetryAttempts: '5'
PendingAnalysisWaitTimeoutMinutes: '5'
- pwsh: |
Copy-Item artifacts\hidi\signing\Microsoft.OpenApi.Hidi.dll src\Microsoft.OpenApi.Hidi\bin\$(buildConfiguration)\net8.0\Microsoft.OpenApi.Hidi.dll
$ErrorActionPreference = 'Stop'
Copy-Item artifacts\hidi\signing\Microsoft.OpenApi.Hidi.exe artifacts\hidi\win-x64\Microsoft.OpenApi.Hidi.exe
New-Item -ItemType Directory -Force '$(Build.ArtifactStagingDirectory)\hidi' | Out-Null
dotnet pack src\Microsoft.OpenApi.Hidi\Microsoft.OpenApi.Hidi.csproj -c $(buildConfiguration) --no-build --include-symbols --include-source /p:SymbolPackageFormat=snupkg -o '$(Build.ArtifactStagingDirectory)\hidi'
dotnet pack src\Microsoft.OpenApi.Hidi\Microsoft.OpenApi.Hidi.csproj -c $(buildConfiguration) --no-build --include-symbols --include-source /p:SymbolPackageFormat=snupkg "/p:HidiSignedAssemblyPath=$(Build.SourcesDirectory)\artifacts\hidi\signing\Microsoft.OpenApi.Hidi.dll" -o '$(Build.ArtifactStagingDirectory)\hidi'
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
& .\scripts\verify-hidi-package-assembly.ps1 -PackagePath '$(Build.ArtifactStagingDirectory)\hidi\Microsoft.OpenApi.Hidi.$(HidiVersion).nupkg' -SignedAssemblyPath '$(Build.SourcesDirectory)\artifacts\hidi\signing\Microsoft.OpenApi.Hidi.dll'
Compress-Archive -Path artifacts\hidi\win-x64\* -DestinationPath '$(Build.ArtifactStagingDirectory)\hidi\hidi-win-x64-$(HidiVersion).zip'
Copy-Item artifacts\hidi\win-x64\Microsoft.OpenApi.Hidi.exe '$(Build.ArtifactStagingDirectory)\hidi\Microsoft.OpenApi.Hidi.exe'
displayName: Pack signed hidi binaries
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ jobs:
- name: Data gatherer
id: data_gatherer
shell: pwsh
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
# Get default branch
$repo = 'microsoft/OpenAPI.NET.OData'
$defaultBranch = Invoke-RestMethod -Method GET -Uri https://api.github.com/repos/$repo | Select-Object -ExpandProperty default_branch
$defaultBranch = Invoke-RestMethod -Method GET -Uri https://api.github.com/repos/$repo -Headers @{ Authorization = "Bearer $env:GITHUB_TOKEN" } | Select-Object -ExpandProperty default_branch
Write-Output "default_branch=$(echo $defaultBranch) >> $GITHUB_OUTPUT"

- name: Conditionals handler
Expand Down
72 changes: 39 additions & 33 deletions .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,59 +60,65 @@ jobs:
- name: Install PowerShell test dependency
shell: pwsh
run: Install-Module Pester -RequiredVersion 5.7.1 -Scope CurrentUser -Force
- name: Test Hidi NuGet helper with measured coverage
- name: Test Hidi NuGet helpers with measured coverage
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
Import-Module Pester -RequiredVersion 5.7.1 -ErrorAction Stop
$helperPath = 'scripts/check-nuget-package-published.ps1'
$helper = (Resolve-Path $helperPath).Path
$helperPaths = @('scripts/check-nuget-package-published.ps1', 'scripts/verify-hidi-package-assembly.ps1')
$helpers = @($helperPaths | ForEach-Object { (Resolve-Path $_).Path })
$outputRoot = Join-Path $PWD 'artifacts\hidi\powershell-coverage'
New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null
$configuration = New-PesterConfiguration
$configuration.Run.Path = 'test\Microsoft.OpenApi.Hidi.Tests\check-nuget-package-published.Tests.ps1'
$configuration.Run.Path = @('test\Microsoft.OpenApi.Hidi.Tests\check-nuget-package-published.Tests.ps1', 'test\Microsoft.OpenApi.Hidi.Tests\verify-hidi-package-assembly.Tests.ps1')
$configuration.Run.PassThru = $true
$configuration.CodeCoverage.Enabled = $true
$configuration.CodeCoverage.Path = $helper
$configuration.CodeCoverage.Path = $helpers
$configuration.CodeCoverage.OutputFormat = 'JaCoCo'
$configuration.CodeCoverage.OutputPath = Join-Path $outputRoot 'pester-coverage.xml'
$configuration.CodeCoverage.CoveragePercentTarget = 80
$result = Invoke-Pester -Configuration $configuration
if ($result.FailedCount -gt 0 -or $result.PassedCount -lt 24) {
throw "Hidi NuGet helper tests failed or did not execute all 24 cases."
if ($result.FailedCount -gt 0 -or $result.PassedCount -lt 45) {
throw "Hidi NuGet helper tests failed or did not execute all 45 cases."
}
$report = [xml](Get-Content $configuration.CodeCoverage.OutputPath.Value -Raw)
$sourceFiles = @($report.SelectNodes('//sourcefile'))
if ($sourceFiles.Count -ne 1 -or $sourceFiles[0].name -ne [IO.Path]::GetFileName($helper)) {
throw 'Expected measured coverage of only the Hidi NuGet helper.'
}
$lines = @($sourceFiles[0].SelectNodes('line'))
$sourceLineCount = @(Get-Content $helper).Count
$lineNumbers = @($lines | ForEach-Object { [int]$_.nr })
if ($lines.Count -eq 0 -or @($lineNumbers | Select-Object -Unique).Count -ne $lines.Count -or
@($lines | Where-Object {
[int]$_.nr -le 0 -or [int]$_.nr -gt $sourceLineCount -or
-not $_.HasAttribute('ci') -or -not $_.HasAttribute('mi') -or
[int]$_.ci -lt 0 -or [int]$_.mi -lt 0 -or ([int]$_.ci + [int]$_.mi) -le 0
}).Count -gt 0) {
throw 'Missing or invalid measured helper coverage lines.'
}
$covered = @($lines | Where-Object { [int]$_.ci -gt 0 }).Count
if ($covered / $lines.Count -lt 0.8) {
throw "Insufficient measured Hidi helper coverage: $covered/$($lines.Count) lines."
if ($sourceFiles.Count -ne $helpers.Count) {
throw 'Expected measured coverage of both Hidi NuGet helpers.'
}
$coverage = [xml]'<coverage version="1"/>'
$file = $coverage.CreateElement('file')
$file.SetAttribute('path', $helperPath)
[void]$coverage.DocumentElement.AppendChild($file)
foreach ($line in $lines) {
$entry = $coverage.CreateElement('lineToCover')
$entry.SetAttribute('lineNumber', $line.nr)
$entry.SetAttribute('covered', ([int]$line.ci -gt 0).ToString().ToLowerInvariant())
[void]$file.AppendChild($entry)
foreach ($helperPath in $helperPaths) {
$helper = (Resolve-Path $helperPath).Path
$source = @($sourceFiles | Where-Object { $_.name -eq [IO.Path]::GetFileName($helper) })
if ($source.Count -ne 1) { throw "Missing or ambiguous measured coverage for $helperPath." }
$lines = @($source[0].SelectNodes('line'))
$sourceLineCount = @(Get-Content $helper).Count
$lineNumbers = @($lines | ForEach-Object { [int]$_.nr })
if ($lines.Count -eq 0 -or @($lineNumbers | Select-Object -Unique).Count -ne $lines.Count -or
@($lines | Where-Object {
[int]$_.nr -le 0 -or [int]$_.nr -gt $sourceLineCount -or
-not $_.HasAttribute('ci') -or -not $_.HasAttribute('mi') -or
[int]$_.ci -lt 0 -or [int]$_.mi -lt 0 -or ([int]$_.ci + [int]$_.mi) -le 0
}).Count -gt 0) {
throw "Missing or invalid measured helper coverage lines for $helperPath."
}
$covered = @($lines | Where-Object { [int]$_.ci -gt 0 }).Count
if ($covered / $lines.Count -lt 0.8) {
throw "Insufficient measured Hidi helper coverage for ${helperPath}: $covered/$($lines.Count) lines."
}
$file = $coverage.CreateElement('file')
$file.SetAttribute('path', $helperPath)
[void]$coverage.DocumentElement.AppendChild($file)
foreach ($line in $lines) {
$entry = $coverage.CreateElement('lineToCover')
$entry.SetAttribute('lineNumber', $line.nr)
$entry.SetAttribute('covered', ([int]$line.ci -gt 0).ToString().ToLowerInvariant())
[void]$file.AppendChild($entry)
}
Write-Host "Measured Hidi NuGet helper coverage for ${helperPath}: $covered/$($lines.Count) lines."
}
$coverage.Save((Join-Path $outputRoot 'sonar-coverage.xml'))
Write-Host "Measured Hidi NuGet helper coverage: $covered/$($lines.Count) lines; Sonar generic report generated."
Write-Host "Sonar generic report generated from measured coverage."
- name: Build and analyze
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,12 +116,12 @@ feed responses fail closed. Releases must use an exact `hidi-v2.<version>` tag
matching the project version and be newer than the `2.12.2` migration baseline.
This pipeline implementation does not enable publishing or authorize resources.

The SonarCloud workflow runs the private-feed helper tests with Pester 5.7.1 and
converts measured JaCoCo line hits to Sonar generic coverage, alongside the
existing C# OpenCover reports. The Pester script resides inside the Hidi test
The SonarCloud workflow runs the private-feed and signed-payload helper tests
with Pester 5.7.1 and converts measured JaCoCo line hits to Sonar generic coverage,
alongside existing C# OpenCover reports. The Pester scripts reside inside the Hidi test
project directory so Sonar assigns it to test sources; a linked sibling file
does not establish that ownership. Run the helper tests locally with
`Import-Module Pester -RequiredVersion 5.7.1; Invoke-Pester .\test\Microsoft.OpenApi.Hidi.Tests\check-nuget-package-published.Tests.ps1`.
`Import-Module Pester -RequiredVersion 5.7.1; Invoke-Pester .\test\Microsoft.OpenApi.Hidi.Tests\*.Tests.ps1`.

---

Expand Down
59 changes: 59 additions & 0 deletions scripts/verify-hidi-package-assembly.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

<#
.SYNOPSIS
Verifies the Hidi tool package contains the exact Microsoft-signed staging DLL.
.DESCRIPTION
Run after tool packing and before NuGet signing. A signed NuGet container does
not prove that its assembly payload retained the ESRP signature.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$PackagePath,
[Parameter(Mandatory = $true)]
[string]$SignedAssemblyPath
)

$ErrorActionPreference = 'Stop'

foreach ($path in @($PackagePath, $SignedAssemblyPath)) {
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
throw "Missing Hidi signing verification input: $path"
}
}

$temporaryDirectory = Join-Path ([IO.Path]::GetTempPath()) ([guid]::NewGuid().ToString())
New-Item -ItemType Directory -Path $temporaryDirectory | Out-Null
$packagedAssembly = Join-Path $temporaryDirectory 'Microsoft.OpenApi.Hidi.dll'
$archive = $null
try {
$archive = [IO.Compression.ZipFile]::OpenRead((Resolve-Path -LiteralPath $PackagePath).Path)
$assemblies = @($archive.Entries | Where-Object { $_.Name -ieq 'Microsoft.OpenApi.Hidi.dll' })
if ($assemblies.Count -ne 1 -or $assemblies[0].FullName -cne 'tools/net8.0/any/Microsoft.OpenApi.Hidi.dll') {
throw 'Expected exactly one Hidi DLL at tools/net8.0/any/Microsoft.OpenApi.Hidi.dll.'
}
[IO.Compression.ZipFileExtensions]::ExtractToFile($assemblies[0], $packagedAssembly)

$stagingHash = (Get-FileHash -LiteralPath $SignedAssemblyPath -Algorithm SHA256).Hash
$packageHash = (Get-FileHash -LiteralPath $packagedAssembly -Algorithm SHA256).Hash
if ($packageHash -cne $stagingHash) {
throw "Packaged Hidi DLL differs from the signed staging DLL: $packageHash != $stagingHash"
}
foreach ($assembly in @($SignedAssemblyPath, $packagedAssembly)) {
$signature = Get-AuthenticodeSignature -LiteralPath $assembly
if ($signature.Status -ne 'Valid' -or
$signature.SignerCertificate.Subject -notmatch '(^|,\s*)O=Microsoft Corporation(,|$)') {
throw "Hidi DLL must have a valid Microsoft Corporation Authenticode signature: $assembly ($($signature.Status))"
}
}
Write-Host "Verified packaged Hidi DLL: valid Microsoft Authenticode signature; signed staging SHA256=$stagingHash"
}
finally {
if ($null -ne $archive) { $archive.Dispose() }
if (Test-Path -LiteralPath $packagedAssembly) {
Remove-Item -LiteralPath $packagedAssembly -Force
}
Remove-Item -LiteralPath $temporaryDirectory -Force
}
21 changes: 21 additions & 0 deletions src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,25 @@
<None Include="./readme.md" Pack="true" PackagePath=""/>
</ItemGroup>

<!-- Tool pack publishes IntermediateAssembly from obj, not the DLL in bin. -->
<Target Name="UseHidiSignedAssemblyForToolPack"
AfterTargets="ComputeFilesToPublish"
Condition="'$(PackAsTool)' == 'true' and '$(HidiSignedAssemblyPath)' != ''">
<Error Condition="!Exists('$(HidiSignedAssemblyPath)')"
Text="Missing signed Hidi assembly: $(HidiSignedAssemblyPath)"/>
<ItemGroup>
<_HidiAssemblyToReplace Include="@(ResolvedFileToPublish)"
Condition="'%(ResolvedFileToPublish.RelativePath)' == '$(TargetFileName)'"/>
</ItemGroup>
<Error Condition="'@(_HidiAssemblyToReplace->Count())' != '1'"
Text="Expected exactly one Hidi assembly in the tool publish inputs."/>
<ItemGroup>
<ResolvedFileToPublish Remove="@(_HidiAssemblyToReplace)"/>
<ResolvedFileToPublish Include="$(HidiSignedAssemblyPath)">
<RelativePath>$(TargetFileName)</RelativePath>
<CopyToPublishDirectory>Always</CopyToPublishDirectory>
</ResolvedFileToPublish>
</ItemGroup>
</Target>

</Project>
15 changes: 15 additions & 0 deletions src/Microsoft.OpenApi.Hidi/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,21 @@ Local/CI builds use the public-only strong-name identity. Official release
artifacts are signed in Azure Pipelines; private signing keys do not belong in
this repository. The migration baseline 2.12.2 is already published. Destination
NuGet, executable, and Docker publishing are disabled until source cutover.
Official tool packing passes `HidiSignedAssemblyPath` to replace the Hidi DLL
in the SDK's `ResolvedFileToPublish` items after `ComputeFilesToPublish`.
`PackAsTool` publishes the intermediate assembly from `obj`, so replacing only
the DLL in `bin` does not preserve its Authenticode signature. The opt-in target
uses the exact ESRP staging DLL without recompiling it; ordinary local packing
and Windows single-file publishing retain their default inputs.
Before NuGet signing, `scripts/verify-hidi-package-assembly.ps1` requires exactly
one DLL at `tools/net8.0/any/Microsoft.OpenApi.Hidi.dll`, verifies its SHA256
matches the staging DLL, and requires valid Microsoft Corporation Authenticode
signatures on both. Missing, mismatched, unsigned, or unverifiable payloads fail
the build. A signed NuGet container alone is not assembly-signature evidence.
Local byte-provenance checks and mocked signature unit tests cannot establish
Microsoft ESRP signing readiness; that requires a publish-disabled official run
after the normally approved merge.

The standard Release Please config tracks Hidi as its own component, with a
separate manifest version, project version, changelog, and `hidi-v2.*` tags.
The first destination package release must advance beyond the baseline and use
Expand Down
Loading
Loading