From 5f0eb3dd53e0891cb73741ad8894ac5bfaeb4984 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:24:06 +0000 Subject: [PATCH 1/4] ci(release): publish OpenAPI packages and symbols through ESRP Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .azure-pipelines/ci-build.yml | 183 +++++++++++++++++++++++++++++----- 1 file changed, 159 insertions(+), 24 deletions(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 2c8cb8735..c9260b06b 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -21,6 +21,7 @@ pr: variables: buildPlatform: 'Any CPU' buildConfiguration: 'Release' + NuGetOrganizationName: 'openapinet' ProductBinPath: '$(Build.SourcesDirectory)\src\Microsoft.OpenApi\bin\$(BuildConfiguration)' REGISTRY: 'msgraphprodregistry.azurecr.io' IMAGE_NAME: 'public/openapi/hidi' @@ -226,7 +227,9 @@ extends: inputs: targetFolder: $(Build.ArtifactStagingDirectory)/Nugets sourceFolder: $(Build.ArtifactStagingDirectory) - content: '*.nupkg' + Contents: | + *.nupkg + *.snupkg # Copy repository files to be used in the deploy stage - task: CopyFiles@2 @@ -265,13 +268,59 @@ extends: pool: vmImage: ubuntu-latest steps: - - task: 1ES.PublishNuget@1 - displayName: 'NuGet push' + - pwsh: | + $pkg = Get-ChildItem -Path "$(Pipeline.Workspace)" -Filter "Microsoft.OpenApi.Hidi.*.nupkg" | Where-Object { $_.Name -match '^Microsoft\.OpenApi\.Hidi\.(\d[\w\.\-]*)\.nupkg$' } | Select-Object -First 1 + if (-not $pkg) { throw "No Microsoft.OpenApi.Hidi nupkg found to publish." } + $version = $pkg.Name -replace '^Microsoft\.OpenApi\.Hidi\.', '' -replace '\.nupkg$', '' + $id = 'microsoft.openapi.hidi' + $uri = "https://api.nuget.org/v3-flatcontainer/$id/index.json" + try { + $resp = Invoke-RestMethod -Uri $uri -Headers @{ 'User-Agent' = 'openapinet-azdo-pipeline' } + if ($resp.versions -contains $version) { + Write-Host "NuGet $id $version already published; skipping ESRP release (idempotent re-run)." + Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]true" + } else { + Write-Host "NuGet $id $version not found on nuget.org; will publish via ESRP." + Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]false" + } + } catch { + if ($_.Exception.Response.StatusCode.value__ -eq 404) { + Write-Host "NuGet $id has no published versions yet; will publish via ESRP." + Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]false" + } else { throw } + } + displayName: 'Check whether NuGet package version already published (idempotent)' + - task: CopyFiles@2 + displayName: 'Stage Hidi NuGet packages for ESRP release' + condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true')) + inputs: + SourceFolder: '$(Pipeline.Workspace)' + Contents: | + Microsoft.OpenApi.Hidi.*.nupkg + Microsoft.OpenApi.Hidi.*.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)/Microsoft.OpenApi.Hidi.*.nupkg' - packageParentPath: '$(Pipeline.Workspace)' - nuGetFeedType: external - publishFeedCredentials: 'OpenAPI Nuget Connection' + 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' - deployment: deploy_lib condition: and(contains(variables['build.SourceBranch'], 'refs/tags/v'), succeeded()) @@ -291,20 +340,60 @@ extends: vmImage: ubuntu-latest steps: - pwsh: | - $fileNames = "$(Pipeline.Workspace)/Microsoft.OpenApi.Hidi.*.nupkg", "$(Pipeline.Workspace)/Microsoft.OpenApi.YamlReader.*.nupkg" - foreach($fileName in $fileNames) { - if(Test-Path $fileName) { - Remove-Item $fileName -Verbose + $pkg = Get-ChildItem -Path "$(Pipeline.Workspace)" -Filter "Microsoft.OpenApi.*.nupkg" | Where-Object { $_.Name -match '^Microsoft\.OpenApi\.(\d[\w\.\-]*)\.nupkg$' } | Select-Object -First 1 + if (-not $pkg) { throw "No Microsoft.OpenApi nupkg found to publish." } + $version = $pkg.Name -replace '^Microsoft\.OpenApi\.', '' -replace '\.nupkg$', '' + $id = 'microsoft.openapi' + $uri = "https://api.nuget.org/v3-flatcontainer/$id/index.json" + try { + $resp = Invoke-RestMethod -Uri $uri -Headers @{ 'User-Agent' = 'openapinet-azdo-pipeline' } + if ($resp.versions -contains $version) { + Write-Host "NuGet $id $version already published; skipping ESRP release (idempotent re-run)." + Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]true" + } else { + Write-Host "NuGet $id $version not found on nuget.org; will publish via ESRP." + Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]false" } + } catch { + if ($_.Exception.Response.StatusCode.value__ -eq 404) { + Write-Host "NuGet $id has no published versions yet; will publish via ESRP." + Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]false" + } else { throw } } - displayName: remove other nupkgs to avoid duplication - - task: 1ES.PublishNuget@1 - displayName: 'NuGet push' + displayName: 'Check whether NuGet package version already published (idempotent)' + - task: CopyFiles@2 + displayName: 'Stage OpenAPI NuGet packages for ESRP release' + condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true')) inputs: - packagesToPush: '$(Pipeline.Workspace)/Microsoft.OpenApi.*.nupkg' - packageParentPath: '$(Pipeline.Workspace)' - nuGetFeedType: external - publishFeedCredentials: 'OpenAPI Nuget Connection' + SourceFolder: '$(Pipeline.Workspace)' + Contents: | + Microsoft.OpenApi.*.nupkg + Microsoft.OpenApi.*.snupkg + !Microsoft.OpenApi.Hidi.* + !Microsoft.OpenApi.YamlReader.* + TargetFolder: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/openapi' + CleanTargetFolder: true + - task: EsrpRelease@14 + displayName: 'ESRP Release - OpenAPI NuGet' + condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true')) + inputs: + 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)/openapi' + 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' - deployment: deploy_yaml_reader condition: and(contains(variables['build.SourceBranch'], 'refs/tags/v'), succeeded()) @@ -323,13 +412,59 @@ extends: pool: vmImage: ubuntu-latest steps: - - task: 1ES.PublishNuget@1 - displayName: 'NuGet push' + - pwsh: | + $pkg = Get-ChildItem -Path "$(Pipeline.Workspace)" -Filter "Microsoft.OpenApi.YamlReader.*.nupkg" | Where-Object { $_.Name -match '^Microsoft\.OpenApi\.YamlReader\.(\d[\w\.\-]*)\.nupkg$' } | Select-Object -First 1 + if (-not $pkg) { throw "No Microsoft.OpenApi.YamlReader nupkg found to publish." } + $version = $pkg.Name -replace '^Microsoft\.OpenApi\.YamlReader\.', '' -replace '\.nupkg$', '' + $id = 'microsoft.openapi.yamlreader' + $uri = "https://api.nuget.org/v3-flatcontainer/$id/index.json" + try { + $resp = Invoke-RestMethod -Uri $uri -Headers @{ 'User-Agent' = 'openapinet-azdo-pipeline' } + if ($resp.versions -contains $version) { + Write-Host "NuGet $id $version already published; skipping ESRP release (idempotent re-run)." + Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]true" + } else { + Write-Host "NuGet $id $version not found on nuget.org; will publish via ESRP." + Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]false" + } + } catch { + if ($_.Exception.Response.StatusCode.value__ -eq 404) { + Write-Host "NuGet $id has no published versions yet; will publish via ESRP." + Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]false" + } else { throw } + } + displayName: 'Check whether NuGet package version already published (idempotent)' + - task: CopyFiles@2 + displayName: 'Stage YAML reader NuGet packages for ESRP release' + condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true')) + inputs: + SourceFolder: '$(Pipeline.Workspace)' + Contents: | + Microsoft.OpenApi.YamlReader.*.nupkg + Microsoft.OpenApi.YamlReader.*.snupkg + TargetFolder: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/yaml-reader' + CleanTargetFolder: true + - task: EsrpRelease@14 + displayName: 'ESRP Release - YAML reader NuGet' + condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true')) inputs: - packagesToPush: '$(Pipeline.Workspace)/Microsoft.OpenApi.YamlReader.*.nupkg' - packageParentPath: '$(Pipeline.Workspace)' - nuGetFeedType: external - publishFeedCredentials: 'OpenAPI Nuget Connection' + 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)/yaml-reader' + 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' - deployment: create_github_release condition: and(contains(variables['build.SourceBranch'], 'refs/tags/v'), succeeded()) From c5b68c5feba3c3951b2e43ec70b9d16bab2b0b9d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:30:23 +0000 Subject: [PATCH 2/4] ci(hidi): authenticate Docker restores using a BuildKit secret Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .azure-pipelines/ci-build.yml | 38 ++++++++++++++++++++++++++++++++++- Dockerfile | 4 +++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index c9260b06b..8abdc36b7 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -594,7 +594,33 @@ extends: displayName: 'Get current date' name: setdate condition: eq(variables['Build.SourceBranch'], variables['PREVIEW_BRANCH']) - + + # Keep feed credentials out of the Docker build context and image layers. + - pwsh: | + if ([string]::IsNullOrWhiteSpace($env:FEED_ACCESS_TOKEN)) { + throw "No Azure Artifacts access token available for the Docker build." + } + $feedAccessToken = [System.Security.SecurityElement]::Escape($env:FEED_ACCESS_TOKEN) + @" + + + + + + + + + + + + + + + "@ | Set-Content -Path "$(Agent.TempDirectory)/hidi-docker.nuget.config" -Encoding UTF8 + displayName: 'Create Docker NuGet config (central feed)' + env: + FEED_ACCESS_TOKEN: $(System.AccessToken) + - script: | docker run --privileged --rm msgraphprodregistry.azurecr.io/tonistiigi/binfmt --install all displayName: "Enable multi-platform builds" @@ -613,6 +639,7 @@ extends: # Using quotes around tags to prevent flag interpretation docker buildx build \ --platform linux/amd64,linux/arm64/v8 \ + --secret id=nuget_config,src="$(Agent.TempDirectory)/hidi-docker.nuget.config" \ --push \ -t "$(REGISTRY)/$(IMAGE_NAME):nightly" \ -t "$(REGISTRY)/$(IMAGE_NAME):${VERSION}.${BUILDDATE}${RUNNUMBER}" \ @@ -625,6 +652,7 @@ extends: echo "Building Docker image for release..." docker buildx build\ --platform linux/amd64,linux/arm64/v8 \ + --secret id=nuget_config,src="$(Agent.TempDirectory)/hidi-docker.nuget.config" \ --push \ -t "$(REGISTRY)/$(IMAGE_NAME):latest" \ -t "$(REGISTRY)/$(IMAGE_NAME):${VERSION}" \ @@ -632,6 +660,14 @@ extends: displayName: 'Build and Push Release Image' condition: contains(variables['Build.SourceBranch'], 'refs/tags/v') + - pwsh: | + $configPath = "$(Agent.TempDirectory)/hidi-docker.nuget.config" + if (Test-Path $configPath) { + Remove-Item $configPath -Force + } + displayName: 'Remove Docker NuGet config' + condition: always() + # once the nuget has been released, fill this form to get the public documentation updated. # https://dev.azure.com/msft-skilling/Content/_workitems/create/User%20Story?templateId=39fb91e3-64a2-4c8a-83db-b2bdf3603dd3&ownerId=c4a28f90-17ae-4384-b514-7273392b082b # https://learn.microsoft.com/en-us/dotnet/api/microsoft.openapi diff --git a/Dockerfile b/Dockerfile index 46cb00637..4e932b975 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,9 @@ COPY ./src ./hidi/src COPY ./Directory.Build.props ./hidi/Directory.Build.props COPY ./README.md ./hidi/README.md WORKDIR /app/hidi -RUN dotnet publish ./src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj -c Release +# CI supplies the private feed config as a secret; local builds use default NuGet sources. +RUN --mount=type=secret,id=nuget_config,target=/app/hidi/NuGet.Config \ + dotnet publish ./src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj -c Release FROM mcr.microsoft.com/dotnet/runtime:8.0-jammy-chiseled AS runtime WORKDIR /app From aedec9b484e8c772528ec9aaa0d976a853eb0301 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 9 Oct 2026 12:46:16 +0000 Subject: [PATCH 3/4] ci(release): share authenticated private-feed version checks Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --- .azure-pipelines/ci-build.yml | 99 ++++++++++++----------------------- scripts/checkNuGetVersion.ps1 | 61 +++++++++++++++++++++ 2 files changed, 95 insertions(+), 65 deletions(-) create mode 100644 scripts/checkNuGetVersion.ps1 diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 8abdc36b7..18cd03163 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -22,6 +22,7 @@ variables: buildPlatform: 'Any CPU' buildConfiguration: 'Release' NuGetOrganizationName: 'openapinet' + privateFeedBaseUrl: 'https://microsoftgraph.pkgs.visualstudio.com/0985d294-5762-4bc2-a565-161ef349ca3e/_packaging/GraphDeveloperExperiences_Public' ProductBinPath: '$(Build.SourcesDirectory)\src\Microsoft.OpenApi\bin\$(BuildConfiguration)' REGISTRY: 'msgraphprodregistry.azurecr.io' IMAGE_NAME: 'public/openapi/hidi' @@ -89,7 +90,7 @@ extends: - + "@ | Set-Content -Path "$(Build.SourcesDirectory)/nuget.config" -Encoding UTF8 @@ -231,6 +232,13 @@ extends: *.nupkg *.snupkg + - task: CopyFiles@2 + displayName: 'Include version-check script in Nugets artifact' + inputs: + SourceFolder: '$(Build.SourcesDirectory)/scripts' + Contents: 'checkNuGetVersion.ps1' + TargetFolder: '$(Build.ArtifactStagingDirectory)/Nugets/scripts' + # Copy repository files to be used in the deploy stage - task: CopyFiles@2 displayName: 'Copy repository files for deploy stage' @@ -268,28 +276,15 @@ extends: pool: vmImage: ubuntu-latest steps: - - pwsh: | - $pkg = Get-ChildItem -Path "$(Pipeline.Workspace)" -Filter "Microsoft.OpenApi.Hidi.*.nupkg" | Where-Object { $_.Name -match '^Microsoft\.OpenApi\.Hidi\.(\d[\w\.\-]*)\.nupkg$' } | Select-Object -First 1 - if (-not $pkg) { throw "No Microsoft.OpenApi.Hidi nupkg found to publish." } - $version = $pkg.Name -replace '^Microsoft\.OpenApi\.Hidi\.', '' -replace '\.nupkg$', '' - $id = 'microsoft.openapi.hidi' - $uri = "https://api.nuget.org/v3-flatcontainer/$id/index.json" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ 'User-Agent' = 'openapinet-azdo-pipeline' } - if ($resp.versions -contains $version) { - Write-Host "NuGet $id $version already published; skipping ESRP release (idempotent re-run)." - Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]true" - } else { - Write-Host "NuGet $id $version not found on nuget.org; will publish via ESRP." - Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]false" - } - } catch { - if ($_.Exception.Response.StatusCode.value__ -eq 404) { - Write-Host "NuGet $id has no published versions yet; will publish via ESRP." - Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]false" - } else { throw } - } + - task: PowerShell@2 displayName: 'Check whether NuGet package version already published (idempotent)' + inputs: + targetType: filePath + filePath: '$(Pipeline.Workspace)/scripts/checkNuGetVersion.ps1' + arguments: '-PackageId "Microsoft.OpenApi.Hidi" -PackageDirectory "$(Pipeline.Workspace)" -NuGetServiceIndexUrl "$(privateFeedBaseUrl)/nuget/v3/index.json"' + pwsh: true + env: + FEED_ACCESS_TOKEN: $(System.AccessToken) - task: CopyFiles@2 displayName: 'Stage Hidi NuGet packages for ESRP release' condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true')) @@ -339,28 +334,15 @@ extends: pool: vmImage: ubuntu-latest steps: - - pwsh: | - $pkg = Get-ChildItem -Path "$(Pipeline.Workspace)" -Filter "Microsoft.OpenApi.*.nupkg" | Where-Object { $_.Name -match '^Microsoft\.OpenApi\.(\d[\w\.\-]*)\.nupkg$' } | Select-Object -First 1 - if (-not $pkg) { throw "No Microsoft.OpenApi nupkg found to publish." } - $version = $pkg.Name -replace '^Microsoft\.OpenApi\.', '' -replace '\.nupkg$', '' - $id = 'microsoft.openapi' - $uri = "https://api.nuget.org/v3-flatcontainer/$id/index.json" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ 'User-Agent' = 'openapinet-azdo-pipeline' } - if ($resp.versions -contains $version) { - Write-Host "NuGet $id $version already published; skipping ESRP release (idempotent re-run)." - Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]true" - } else { - Write-Host "NuGet $id $version not found on nuget.org; will publish via ESRP." - Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]false" - } - } catch { - if ($_.Exception.Response.StatusCode.value__ -eq 404) { - Write-Host "NuGet $id has no published versions yet; will publish via ESRP." - Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]false" - } else { throw } - } + - task: PowerShell@2 displayName: 'Check whether NuGet package version already published (idempotent)' + inputs: + targetType: filePath + filePath: '$(Pipeline.Workspace)/scripts/checkNuGetVersion.ps1' + arguments: '-PackageId "Microsoft.OpenApi" -PackageDirectory "$(Pipeline.Workspace)" -NuGetServiceIndexUrl "$(privateFeedBaseUrl)/nuget/v3/index.json"' + pwsh: true + env: + FEED_ACCESS_TOKEN: $(System.AccessToken) - task: CopyFiles@2 displayName: 'Stage OpenAPI NuGet packages for ESRP release' condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true')) @@ -412,28 +394,15 @@ extends: pool: vmImage: ubuntu-latest steps: - - pwsh: | - $pkg = Get-ChildItem -Path "$(Pipeline.Workspace)" -Filter "Microsoft.OpenApi.YamlReader.*.nupkg" | Where-Object { $_.Name -match '^Microsoft\.OpenApi\.YamlReader\.(\d[\w\.\-]*)\.nupkg$' } | Select-Object -First 1 - if (-not $pkg) { throw "No Microsoft.OpenApi.YamlReader nupkg found to publish." } - $version = $pkg.Name -replace '^Microsoft\.OpenApi\.YamlReader\.', '' -replace '\.nupkg$', '' - $id = 'microsoft.openapi.yamlreader' - $uri = "https://api.nuget.org/v3-flatcontainer/$id/index.json" - try { - $resp = Invoke-RestMethod -Uri $uri -Headers @{ 'User-Agent' = 'openapinet-azdo-pipeline' } - if ($resp.versions -contains $version) { - Write-Host "NuGet $id $version already published; skipping ESRP release (idempotent re-run)." - Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]true" - } else { - Write-Host "NuGet $id $version not found on nuget.org; will publish via ESRP." - Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]false" - } - } catch { - if ($_.Exception.Response.StatusCode.value__ -eq 404) { - Write-Host "NuGet $id has no published versions yet; will publish via ESRP." - Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]false" - } else { throw } - } + - task: PowerShell@2 displayName: 'Check whether NuGet package version already published (idempotent)' + inputs: + targetType: filePath + filePath: '$(Pipeline.Workspace)/scripts/checkNuGetVersion.ps1' + arguments: '-PackageId "Microsoft.OpenApi.YamlReader" -PackageDirectory "$(Pipeline.Workspace)" -NuGetServiceIndexUrl "$(privateFeedBaseUrl)/nuget/v3/index.json"' + pwsh: true + env: + FEED_ACCESS_TOKEN: $(System.AccessToken) - task: CopyFiles@2 displayName: 'Stage YAML reader NuGet packages for ESRP release' condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true')) @@ -606,7 +575,7 @@ extends: - + diff --git a/scripts/checkNuGetVersion.ps1 b/scripts/checkNuGetVersion.ps1 new file mode 100644 index 000000000..02a261bc1 --- /dev/null +++ b/scripts/checkNuGetVersion.ps1 @@ -0,0 +1,61 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +param( + [Parameter(Mandatory = $true)] + [string]$PackageId, + [Parameter(Mandatory = $true)] + [string]$PackageDirectory, + [Parameter(Mandatory = $true)] + [string]$NuGetServiceIndexUrl +) + +$ErrorActionPreference = 'Stop' + +if ([string]::IsNullOrWhiteSpace($env:FEED_ACCESS_TOKEN)) { + throw 'No Azure Artifacts access token available for the version check.' +} + +$packagePattern = '^' + [regex]::Escape($PackageId) + '\.(\d[\w\.\-]*)\.nupkg$' +$packages = @(Get-ChildItem -Path $PackageDirectory -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:$($env:FEED_ACCESS_TOKEN)")) +$headers = @{ + 'User-Agent' = 'openapinet-azdo-pipeline' + 'Authorization' = "Basic $credentials" +} + +$serviceIndex = Invoke-RestMethod -Uri $NuGetServiceIndexUrl -Headers $headers +$packageBaseAddress = $serviceIndex.resources | + Where-Object { $_.'@type' -eq 'PackageBaseAddress/3.0.0' } | + Select-Object -First 1 -ExpandProperty '@id' +if ([string]::IsNullOrWhiteSpace($packageBaseAddress)) { + throw 'The private NuGet feed does not advertise a package base address.' +} + +$uri = "$($packageBaseAddress.TrimEnd('/'))/$id/index.json" +try { + $response = Invoke-RestMethod -Uri $uri -Headers $headers + if ($null -eq $response.versions) { + throw 'The private NuGet feed returned an invalid package version response.' + } + $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())" From 8ed2fbc743b4c33c3a13daca2a87279e2f7bae61 Mon Sep 17 00:00:00 2001 From: Vincent Biret Date: Fri, 9 Oct 2026 09:11:03 -0400 Subject: [PATCH 4/4] ci(release): align NuGet publication checks across repositories Use the identical check-nuget-package-published.ps1 script shared with microsoft/kiota#8380. Preserve Basic authentication and exact-one-artifact validation while enforcing private HTTPS endpoints and blocking redirects. Update script packaging and all three deployment call sites. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 4b06c7e0-7ef0-4ea7-b3f3-ed248a9e6ce9 --- .azure-pipelines/ci-build.yml | 8 +-- scripts/check-nuget-package-published.ps1 | 84 +++++++++++++++++++++++ scripts/checkNuGetVersion.ps1 | 61 ---------------- 3 files changed, 88 insertions(+), 65 deletions(-) create mode 100644 scripts/check-nuget-package-published.ps1 delete mode 100644 scripts/checkNuGetVersion.ps1 diff --git a/.azure-pipelines/ci-build.yml b/.azure-pipelines/ci-build.yml index 18cd03163..5e8a40e98 100644 --- a/.azure-pipelines/ci-build.yml +++ b/.azure-pipelines/ci-build.yml @@ -236,7 +236,7 @@ extends: displayName: 'Include version-check script in Nugets artifact' inputs: SourceFolder: '$(Build.SourcesDirectory)/scripts' - Contents: 'checkNuGetVersion.ps1' + Contents: 'check-nuget-package-published.ps1' TargetFolder: '$(Build.ArtifactStagingDirectory)/Nugets/scripts' # Copy repository files to be used in the deploy stage @@ -280,7 +280,7 @@ extends: displayName: 'Check whether NuGet package version already published (idempotent)' inputs: targetType: filePath - filePath: '$(Pipeline.Workspace)/scripts/checkNuGetVersion.ps1' + filePath: '$(Pipeline.Workspace)/scripts/check-nuget-package-published.ps1' arguments: '-PackageId "Microsoft.OpenApi.Hidi" -PackageDirectory "$(Pipeline.Workspace)" -NuGetServiceIndexUrl "$(privateFeedBaseUrl)/nuget/v3/index.json"' pwsh: true env: @@ -338,7 +338,7 @@ extends: displayName: 'Check whether NuGet package version already published (idempotent)' inputs: targetType: filePath - filePath: '$(Pipeline.Workspace)/scripts/checkNuGetVersion.ps1' + filePath: '$(Pipeline.Workspace)/scripts/check-nuget-package-published.ps1' arguments: '-PackageId "Microsoft.OpenApi" -PackageDirectory "$(Pipeline.Workspace)" -NuGetServiceIndexUrl "$(privateFeedBaseUrl)/nuget/v3/index.json"' pwsh: true env: @@ -398,7 +398,7 @@ extends: displayName: 'Check whether NuGet package version already published (idempotent)' inputs: targetType: filePath - filePath: '$(Pipeline.Workspace)/scripts/checkNuGetVersion.ps1' + filePath: '$(Pipeline.Workspace)/scripts/check-nuget-package-published.ps1' arguments: '-PackageId "Microsoft.OpenApi.YamlReader" -PackageDirectory "$(Pipeline.Workspace)" -NuGetServiceIndexUrl "$(privateFeedBaseUrl)/nuget/v3/index.json"' pwsh: true env: diff --git a/scripts/check-nuget-package-published.ps1 b/scripts/check-nuget-package-published.ps1 new file mode 100644 index 000000000..84f45419d --- /dev/null +++ b/scripts/check-nuget-package-published.ps1 @@ -0,0 +1,84 @@ +# 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. +#> +[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." + } + $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())" diff --git a/scripts/checkNuGetVersion.ps1 b/scripts/checkNuGetVersion.ps1 deleted file mode 100644 index 02a261bc1..000000000 --- a/scripts/checkNuGetVersion.ps1 +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (c) Microsoft Corporation. All rights reserved. -# Licensed under the MIT License. - -param( - [Parameter(Mandatory = $true)] - [string]$PackageId, - [Parameter(Mandatory = $true)] - [string]$PackageDirectory, - [Parameter(Mandatory = $true)] - [string]$NuGetServiceIndexUrl -) - -$ErrorActionPreference = 'Stop' - -if ([string]::IsNullOrWhiteSpace($env:FEED_ACCESS_TOKEN)) { - throw 'No Azure Artifacts access token available for the version check.' -} - -$packagePattern = '^' + [regex]::Escape($PackageId) + '\.(\d[\w\.\-]*)\.nupkg$' -$packages = @(Get-ChildItem -Path $PackageDirectory -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:$($env:FEED_ACCESS_TOKEN)")) -$headers = @{ - 'User-Agent' = 'openapinet-azdo-pipeline' - 'Authorization' = "Basic $credentials" -} - -$serviceIndex = Invoke-RestMethod -Uri $NuGetServiceIndexUrl -Headers $headers -$packageBaseAddress = $serviceIndex.resources | - Where-Object { $_.'@type' -eq 'PackageBaseAddress/3.0.0' } | - Select-Object -First 1 -ExpandProperty '@id' -if ([string]::IsNullOrWhiteSpace($packageBaseAddress)) { - throw 'The private NuGet feed does not advertise a package base address.' -} - -$uri = "$($packageBaseAddress.TrimEnd('/'))/$id/index.json" -try { - $response = Invoke-RestMethod -Uri $uri -Headers $headers - if ($null -eq $response.versions) { - throw 'The private NuGet feed returned an invalid package version response.' - } - $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())"