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 the destination PR lands and source publishing is cut over.
hidiPublishingEnabled: 'false'

Expand Down Expand Up @@ -162,7 +164,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 @@ -200,6 +202,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-v2.'))
Expand All @@ -222,15 +230,50 @@ extends:
if ([version]($tag -split '-')[0] -le [version]'2.12.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"
dotnet workload restore
dotnet build
dotnet test test\Microsoft.OpenAPI.OData.Reader.Tests\Microsoft.OpenAPI.OData.Reader.Tests.csproj --no-build --verbosity normal /p:CollectCoverage=true /p:CoverletOutputFormat=opencover
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,20 @@ imported commits can also be retained by the later main-branch migration.
Destination hidi release and production publishing are disabled until source
cutover; OpenAPI.NET remains the publisher in the meantime.

The gated hidi NuGet release uses `EsrpRelease@14`, staging only the exact
`Microsoft.OpenApi.Hidi` package and its `.snupkg` symbols from the Hidi build
artifact. The authenticated private-feed version check is ported from
[microsoft/OpenAPI.NET#3107](https://github.com/microsoft/OpenAPI.NET/pull/3107).
An existing version skips ESRP on a re-run; authentication, network, and malformed
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. Run the helper tests locally with
`Import-Module Pester -RequiredVersion 5.7.1; Invoke-Pester .\test\scripts\check-nuget-package-published.Tests.ps1`.

---

# Contributing
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())"
Loading
Loading