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
57 changes: 50 additions & 7 deletions .azure-pipelines/hidi-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ pr:

variables:
buildConfiguration: Release
NuGetOrganizationName: 'openapinet'
privateFeedBaseUrl: 'https://microsoftgraph.pkgs.visualstudio.com/0985d294-5762-4bc2-a565-161ef349ca3e/_packaging/GraphDeveloperExperiences_Public'
# Keep OFF until destination merges and source publishing is cut over.
hidiPublishingEnabled: 'false'
hidiPreviewPublishingEnabled: 'false'
Expand Down Expand Up @@ -182,7 +184,7 @@ extends:
Copy-Item artifacts\hidi\signing\Microsoft.OpenApi.Hidi.dll src\Microsoft.OpenApi.Hidi\bin\$(buildConfiguration)\net8.0\Microsoft.OpenApi.Hidi.dll
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 -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 -o '$(Build.ArtifactStagingDirectory)\hidi'
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
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'
Expand Down Expand Up @@ -220,6 +222,12 @@ extends:
MaxConcurrency: '50'
MaxRetryAttempts: '5'
PendingAnalysisWaitTimeoutMinutes: '5'
- task: CopyFiles@2
displayName: Include private-feed version check in Hidi artifact
inputs:
SourceFolder: '$(Build.SourcesDirectory)\scripts'
Contents: 'check-nuget-package-published.ps1'
TargetFolder: '$(Build.ArtifactStagingDirectory)\hidi\scripts'
- stage: publish
dependsOn: build
condition: and(succeeded(), eq(variables.hidiPublishingEnabled, 'true'), startsWith(variables['Build.SourceBranch'], 'refs/tags/hidi-v3.'))
Expand All @@ -242,15 +250,50 @@ extends:
if ([version]($tag -split '-')[0] -le [version]'3.10.2') { throw "Never republish the source migration baseline or an older hidi version." }
$package = "$(Pipeline.Workspace)\hidi\Microsoft.OpenApi.Hidi.$tag.nupkg"
if (-not (Test-Path $package)) { throw "Missing exact hidi package: $package" }
$symbols = "$(Pipeline.Workspace)\hidi\Microsoft.OpenApi.Hidi.$tag.snupkg"
if (-not (Test-Path $symbols)) { throw "Missing exact hidi symbols: $symbols" }
Write-Host "##vso[task.setvariable variable=HidiReleaseVersion]$tag"
displayName: Verify new hidi release version and exact package
- task: 1ES.PublishNuget@1
displayName: Publish hidi NuGet tool
- task: PowerShell@2
displayName: Check whether Hidi NuGet version is already published
inputs:
targetType: filePath
filePath: '$(Pipeline.Workspace)\hidi\scripts\check-nuget-package-published.ps1'
arguments: '-PackageId "Microsoft.OpenApi.Hidi" -PackageDirectory "$(Pipeline.Workspace)\hidi" -NuGetServiceIndexUrl "$(privateFeedBaseUrl)/nuget/v3/index.json"'
pwsh: true
env:
FEED_ACCESS_TOKEN: $(System.AccessToken)
- task: CopyFiles@2
displayName: Stage exact Hidi NuGet packages for ESRP release
condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true'))
inputs:
SourceFolder: '$(Pipeline.Workspace)\hidi'
Contents: |
Microsoft.OpenApi.Hidi.$(HidiReleaseVersion).nupkg
Microsoft.OpenApi.Hidi.$(HidiReleaseVersion).snupkg
TargetFolder: '$(Pipeline.Workspace)\nuget-packages\$(NuGetOrganizationName)\hidi'
CleanTargetFolder: true
- task: EsrpRelease@14
displayName: ESRP Release - Hidi NuGet
condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true'))
inputs:
packagesToPush: '$(Pipeline.Workspace)\hidi\Microsoft.OpenApi.Hidi.$(HidiReleaseVersion).nupkg'
nuGetFeedType: external
publishFeedCredentials: OpenAPI Nuget Connection
packageParentPath: '$(Pipeline.Workspace)\hidi'
connectedservicename: 'Federated DevX ESRP Managed Identity Connection'
usemanagedidentity: false
keyvaultname: 'akv-prod-eastus'
authcertname: 'ReferenceLibraryPrivateCert'
signcertname: 'ReferencePackagePublisherCertificate'
clientid: '65035b7f-7357-4f29-bf25-c5ee5c3949f8'
intent: 'packagedistribution'
contenttype: 'NuGet'
organizationname: '$(NuGetOrganizationName)'
contentsource: 'Folder'
folderlocation: '$(Pipeline.Workspace)\nuget-packages\$(NuGetOrganizationName)\hidi'
waitforreleasecompletion: true
owners: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com'
approvers: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com'
serviceendpointurl: 'https://api.esrp.microsoft.com/'
mainpublisher: 'ESRPRELPACMAN'
domaintenantid: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2'
- task: GitHubRelease@1
displayName: Attach signed hidi release artifacts
inputs:
Expand Down
58 changes: 57 additions & 1 deletion .github/workflows/sonarcloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,70 @@ jobs:
restore-keys: ${{ runner.os }}-sonar
- name: Install SonarCloud scanner
run: dotnet tool install dotnet-sonarscanner --create-manifest-if-needed
- 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
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
$outputRoot = Join-Path $PWD 'artifacts\hidi\powershell-coverage'
New-Item -ItemType Directory -Force -Path $outputRoot | Out-Null
$configuration = New-PesterConfiguration
$configuration.Run.Path = 'test\scripts\check-nuget-package-published.Tests.ps1'
$configuration.Run.PassThru = $true
$configuration.CodeCoverage.Enabled = $true
$configuration.CodeCoverage.Path = $helper
$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."
}
$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."
}
$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)
}
$coverage.Save((Join-Path $outputRoot 'sonar-coverage.xml'))
Write-Host "Measured Hidi NuGet helper coverage: $covered/$($lines.Count) lines; Sonar generic report generated."
- name: Build and analyze
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
CollectCoverage: true
CoverletOutputFormat: 'opencover' # https://github.com/microsoft/vstest/issues/4014#issuecomment-1307913682
shell: pwsh
run: |
dotnet tool run dotnet-sonarscanner begin /k:"microsoft_OpenAPI.NET.OData" /o:"microsoft" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="test/**/coverage.net8.0.opencover.xml,artifacts/hidi/coverage/*opencover*.xml"
dotnet tool run dotnet-sonarscanner begin /k:"microsoft_OpenAPI.NET.OData" /o:"microsoft" /d:sonar.token="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.opencover.reportsPaths="test/**/coverage.net8.0.opencover.xml,artifacts/hidi/coverage/*opencover*.xml" /d:sonar.coverageReportPaths="artifacts/hidi/powershell-coverage/sonar-coverage.xml"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
dotnet workload restore
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
Expand Down
93 changes: 93 additions & 0 deletions scripts/check-nuget-package-published.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

<#
.SYNOPSIS
Checks whether a NuGet artifact's version exists in an authenticated Azure Artifacts feed.
.DESCRIPTION
Resolves the package content endpoint from the private feed's NuGet v3 service index.
Sets nugetAlreadyPublished for the ESRP release steps; only a missing package or version
permits publishing. Feed authentication and other lookup failures fail the step.
.NOTES
Ported from microsoft/OpenAPI.NET scripts/check-nuget-package-published.ps1
at e1a75437b76ebfc7c9eb446e9fd0b59a21afb14b (#3107), with fail-closed validation
of malformed feed version lists.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$PackageDirectory,
[Parameter(Mandatory = $true)]
[string]$PackageId,
[Parameter(Mandatory = $true)]
[string]$NuGetServiceIndexUrl,
[string]$FeedAccessToken = $env:FEED_ACCESS_TOKEN
)

$ErrorActionPreference = 'Stop'

function Assert-PrivateFeedUrl {
param([string]$Url)

$uri = [uri]$Url
if (-not $uri.IsAbsoluteUri -or $uri.Scheme -ne 'https' -or
($uri.Host -ne 'pkgs.dev.azure.com' -and -not $uri.Host.EndsWith('.pkgs.visualstudio.com'))) {
throw "NuGet lookups must use an HTTPS Azure Artifacts feed: $Url"
}
}

Assert-PrivateFeedUrl -Url $NuGetServiceIndexUrl
if ([string]::IsNullOrWhiteSpace($FeedAccessToken)) {
throw 'FEED_ACCESS_TOKEN is required to query the private NuGet feed.'
}

$packagePattern = '^' + [regex]::Escape($PackageId) + '\.(\d[\w\.\-]*)\.nupkg$'
$packages = @(Get-ChildItem -Path $PackageDirectory -File -Filter "$PackageId.*.nupkg" |
Where-Object { $_.Name -match $packagePattern })
if ($packages.Count -ne 1) {
throw "Expected exactly one $PackageId nupkg to publish; found $($packages.Count)."
}
$version = [regex]::Match($packages[0].Name, $packagePattern, 'IgnoreCase').Groups[1].Value
$id = $PackageId.ToLowerInvariant()
$credentials = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("AzureDevOps:$FeedAccessToken"))
$headers = @{
'Authorization' = "Basic $credentials"
'User-Agent' = 'openapi-azdo-pipeline'
}

$index = Invoke-RestMethod -Uri $NuGetServiceIndexUrl -Headers $headers -MaximumRedirection 0
$resource = $index.resources | Where-Object { $_.'@type' -eq 'PackageBaseAddress/3.0.0' } | Select-Object -First 1
if ([string]::IsNullOrWhiteSpace($resource.'@id')) {
throw "No PackageBaseAddress resource found in the NuGet service index at $NuGetServiceIndexUrl"
}
$uri = "$($resource.'@id'.TrimEnd('/'))/$id/index.json"
Assert-PrivateFeedUrl -Url $uri

try {
$response = Invoke-RestMethod -Uri $uri -Headers $headers -MaximumRedirection 0
if ($null -eq $response.versions) {
throw "No versions returned for NuGet $id by the private feed."
}
if ($response.versions -isnot [array] -or @($response.versions | Where-Object {
$_ -isnot [string] -or $_ -notmatch '^\d[\w\.\-]*$'
}).Count -gt 0) {
throw "Invalid version list returned for NuGet $id by the private feed."
}
$alreadyPublished = $response.versions -contains $version
}
catch {
if ([int]$_.Exception.Response.StatusCode -eq 404) {
$alreadyPublished = $false
}
else {
throw
}
}

if ($alreadyPublished) {
Write-Host "NuGet $id $version already present in the private feed; skipping ESRP release (idempotent re-run)."
}
else {
Write-Host "NuGet $id $version not found in the private feed; will publish via ESRP."
}
Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]$($alreadyPublished.ToString().ToLowerInvariant())"
29 changes: 29 additions & 0 deletions src/Microsoft.OpenApi.Hidi/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,35 @@ Repository-wide MTP migration is tracked separately in
[#885](https://github.com/microsoft/OpenAPI.NET.OData/issues/885); this migration
does not change the existing OData test platform.

### Official NuGet releases

The gated main Hidi publisher preserves the ESRP NuGet contract from
[microsoft/OpenAPI.NET#3107](https://github.com/microsoft/OpenAPI.NET/pull/3107)
(source commit `e1a75437b76ebfc7c9eb446e9fd0b59a21afb14b`). Official builds include
the signed Hidi `.nupkg`, a matching `.snupkg` and the private-feed version-check
script in the `Hidi` artifact. The `nuget-org` release job consumes that artifact
without a repository checkout or the Docker context.

Before ESRP publication, the job requires the exact `hidi-v3.*` release package
and symbols with a version newer than 3.10.2. It checks
`GraphDeveloperExperiences_Public` using `System.AccessToken` through
`FEED_ACCESS_TOKEN`; only a missing package (HTTP 404) or version permits
publication. Authentication, network and other feed failures stop the job.
An existing version skips ESRP publication but still allows the existing GitHub
release's signed package, executable and ZIP attachments to be updated.

ESRP receives only the exact Hidi package and matching symbols under the
`openapinet` organization, retaining the source's federated DevX connection,
publisher identity, owners and approvers. This handoff does not enable publishing
or grant pipeline permissions. Owners must separately verify private-feed token
access, the ESRP connection and protected `nuget-org` environment authorization
before cutover; local packaging does not prove production signing or publishing.

The private-feed helper has isolated Pester 5.7.1 tests (no live feed access).
Run `Invoke-Pester .\test\scripts\check-nuget-package-published.Tests.ps1` to
execute the cases. The Sonar workflow also measures helper line coverage and
imports a generic coverage report alongside the existing C# OpenCover reports.

### Windows executable

```powershell
Expand Down
Loading
Loading