Skip to content

Commit e1a7543

Browse files
Copilotbaywet
andauthored
ci(release): publish NuGet via ESRP and authenticate Hidi Docker restores (#3107)
* ci(release): publish OpenAPI packages and symbols through ESRP Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> * ci(hidi): authenticate Docker restores using a BuildKit secret Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> * ci(release): share authenticated private-feed version checks Co-authored-by: baywet <7905502+baywet@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: baywet <7905502+baywet@users.noreply.github.com>
1 parent 15dca96 commit e1a7543

3 files changed

Lines changed: 256 additions & 30 deletions

File tree

‎.azure-pipelines/ci-build.yml‎

Lines changed: 169 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ pr:
2121
variables:
2222
buildPlatform: 'Any CPU'
2323
buildConfiguration: 'Release'
24+
NuGetOrganizationName: 'openapinet'
25+
privateFeedBaseUrl: 'https://microsoftgraph.pkgs.visualstudio.com/0985d294-5762-4bc2-a565-161ef349ca3e/_packaging/GraphDeveloperExperiences_Public'
2426
ProductBinPath: '$(Build.SourcesDirectory)\src\Microsoft.OpenApi\bin\$(BuildConfiguration)'
2527
REGISTRY: 'msgraphprodregistry.azurecr.io'
2628
IMAGE_NAME: 'public/openapi/hidi'
@@ -88,7 +90,7 @@ extends:
8890
<configuration>
8991
<packageSources>
9092
<clear />
91-
<add key="GraphDeveloperExperiences_Public" value="https://microsoftgraph.pkgs.visualstudio.com/0985d294-5762-4bc2-a565-161ef349ca3e/_packaging/GraphDeveloperExperiences_Public/nuget/v3/index.json" />
93+
<add key="GraphDeveloperExperiences_Public" value="$(privateFeedBaseUrl)/nuget/v3/index.json" />
9294
</packageSources>
9395
</configuration>
9496
"@ | Set-Content -Path "$(Build.SourcesDirectory)/nuget.config" -Encoding UTF8
@@ -226,7 +228,16 @@ extends:
226228
inputs:
227229
targetFolder: $(Build.ArtifactStagingDirectory)/Nugets
228230
sourceFolder: $(Build.ArtifactStagingDirectory)
229-
content: '*.nupkg'
231+
Contents: |
232+
*.nupkg
233+
*.snupkg
234+
235+
- task: CopyFiles@2
236+
displayName: 'Include version-check script in Nugets artifact'
237+
inputs:
238+
SourceFolder: '$(Build.SourcesDirectory)/scripts'
239+
Contents: 'check-nuget-package-published.ps1'
240+
TargetFolder: '$(Build.ArtifactStagingDirectory)/Nugets/scripts'
230241

231242
# Copy repository files to be used in the deploy stage
232243
- task: CopyFiles@2
@@ -265,13 +276,46 @@ extends:
265276
pool:
266277
vmImage: ubuntu-latest
267278
steps:
268-
- task: 1ES.PublishNuget@1
269-
displayName: 'NuGet push'
279+
- task: PowerShell@2
280+
displayName: 'Check whether NuGet package version already published (idempotent)'
281+
inputs:
282+
targetType: filePath
283+
filePath: '$(Pipeline.Workspace)/scripts/check-nuget-package-published.ps1'
284+
arguments: '-PackageId "Microsoft.OpenApi.Hidi" -PackageDirectory "$(Pipeline.Workspace)" -NuGetServiceIndexUrl "$(privateFeedBaseUrl)/nuget/v3/index.json"'
285+
pwsh: true
286+
env:
287+
FEED_ACCESS_TOKEN: $(System.AccessToken)
288+
- task: CopyFiles@2
289+
displayName: 'Stage Hidi NuGet packages for ESRP release'
290+
condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true'))
291+
inputs:
292+
SourceFolder: '$(Pipeline.Workspace)'
293+
Contents: |
294+
Microsoft.OpenApi.Hidi.*.nupkg
295+
Microsoft.OpenApi.Hidi.*.snupkg
296+
TargetFolder: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/hidi'
297+
CleanTargetFolder: true
298+
- task: EsrpRelease@14
299+
displayName: 'ESRP Release - Hidi NuGet'
300+
condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true'))
270301
inputs:
271-
packagesToPush: '$(Pipeline.Workspace)/Microsoft.OpenApi.Hidi.*.nupkg'
272-
packageParentPath: '$(Pipeline.Workspace)'
273-
nuGetFeedType: external
274-
publishFeedCredentials: 'OpenAPI Nuget Connection'
302+
connectedservicename: 'Federated DevX ESRP Managed Identity Connection'
303+
usemanagedidentity: false
304+
keyvaultname: 'akv-prod-eastus'
305+
authcertname: 'ReferenceLibraryPrivateCert'
306+
signcertname: 'ReferencePackagePublisherCertificate'
307+
clientid: '65035b7f-7357-4f29-bf25-c5ee5c3949f8'
308+
intent: 'packagedistribution'
309+
contenttype: 'NuGet'
310+
organizationname: '$(NuGetOrganizationName)'
311+
contentsource: 'Folder'
312+
folderlocation: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/hidi'
313+
waitforreleasecompletion: true
314+
owners: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com'
315+
approvers: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com'
316+
serviceendpointurl: 'https://api.esrp.microsoft.com/'
317+
mainpublisher: 'ESRPRELPACMAN'
318+
domaintenantid: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2'
275319

276320
- deployment: deploy_lib
277321
condition: and(contains(variables['build.SourceBranch'], 'refs/tags/v'), succeeded())
@@ -290,21 +334,48 @@ extends:
290334
pool:
291335
vmImage: ubuntu-latest
292336
steps:
293-
- pwsh: |
294-
$fileNames = "$(Pipeline.Workspace)/Microsoft.OpenApi.Hidi.*.nupkg", "$(Pipeline.Workspace)/Microsoft.OpenApi.YamlReader.*.nupkg"
295-
foreach($fileName in $fileNames) {
296-
if(Test-Path $fileName) {
297-
Remove-Item $fileName -Verbose
298-
}
299-
}
300-
displayName: remove other nupkgs to avoid duplication
301-
- task: 1ES.PublishNuget@1
302-
displayName: 'NuGet push'
337+
- task: PowerShell@2
338+
displayName: 'Check whether NuGet package version already published (idempotent)'
339+
inputs:
340+
targetType: filePath
341+
filePath: '$(Pipeline.Workspace)/scripts/check-nuget-package-published.ps1'
342+
arguments: '-PackageId "Microsoft.OpenApi" -PackageDirectory "$(Pipeline.Workspace)" -NuGetServiceIndexUrl "$(privateFeedBaseUrl)/nuget/v3/index.json"'
343+
pwsh: true
344+
env:
345+
FEED_ACCESS_TOKEN: $(System.AccessToken)
346+
- task: CopyFiles@2
347+
displayName: 'Stage OpenAPI NuGet packages for ESRP release'
348+
condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true'))
349+
inputs:
350+
SourceFolder: '$(Pipeline.Workspace)'
351+
Contents: |
352+
Microsoft.OpenApi.*.nupkg
353+
Microsoft.OpenApi.*.snupkg
354+
!Microsoft.OpenApi.Hidi.*
355+
!Microsoft.OpenApi.YamlReader.*
356+
TargetFolder: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/openapi'
357+
CleanTargetFolder: true
358+
- task: EsrpRelease@14
359+
displayName: 'ESRP Release - OpenAPI NuGet'
360+
condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true'))
303361
inputs:
304-
packagesToPush: '$(Pipeline.Workspace)/Microsoft.OpenApi.*.nupkg'
305-
packageParentPath: '$(Pipeline.Workspace)'
306-
nuGetFeedType: external
307-
publishFeedCredentials: 'OpenAPI Nuget Connection'
362+
connectedservicename: 'Federated DevX ESRP Managed Identity Connection'
363+
usemanagedidentity: false
364+
keyvaultname: 'akv-prod-eastus'
365+
authcertname: 'ReferenceLibraryPrivateCert'
366+
signcertname: 'ReferencePackagePublisherCertificate'
367+
clientid: '65035b7f-7357-4f29-bf25-c5ee5c3949f8'
368+
intent: 'packagedistribution'
369+
contenttype: 'NuGet'
370+
organizationname: '$(NuGetOrganizationName)'
371+
contentsource: 'Folder'
372+
folderlocation: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/openapi'
373+
waitforreleasecompletion: true
374+
owners: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com'
375+
approvers: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com'
376+
serviceendpointurl: 'https://api.esrp.microsoft.com/'
377+
mainpublisher: 'ESRPRELPACMAN'
378+
domaintenantid: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2'
308379

309380
- deployment: deploy_yaml_reader
310381
condition: and(contains(variables['build.SourceBranch'], 'refs/tags/v'), succeeded())
@@ -323,13 +394,46 @@ extends:
323394
pool:
324395
vmImage: ubuntu-latest
325396
steps:
326-
- task: 1ES.PublishNuget@1
327-
displayName: 'NuGet push'
397+
- task: PowerShell@2
398+
displayName: 'Check whether NuGet package version already published (idempotent)'
328399
inputs:
329-
packagesToPush: '$(Pipeline.Workspace)/Microsoft.OpenApi.YamlReader.*.nupkg'
330-
packageParentPath: '$(Pipeline.Workspace)'
331-
nuGetFeedType: external
332-
publishFeedCredentials: 'OpenAPI Nuget Connection'
400+
targetType: filePath
401+
filePath: '$(Pipeline.Workspace)/scripts/check-nuget-package-published.ps1'
402+
arguments: '-PackageId "Microsoft.OpenApi.YamlReader" -PackageDirectory "$(Pipeline.Workspace)" -NuGetServiceIndexUrl "$(privateFeedBaseUrl)/nuget/v3/index.json"'
403+
pwsh: true
404+
env:
405+
FEED_ACCESS_TOKEN: $(System.AccessToken)
406+
- task: CopyFiles@2
407+
displayName: 'Stage YAML reader NuGet packages for ESRP release'
408+
condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true'))
409+
inputs:
410+
SourceFolder: '$(Pipeline.Workspace)'
411+
Contents: |
412+
Microsoft.OpenApi.YamlReader.*.nupkg
413+
Microsoft.OpenApi.YamlReader.*.snupkg
414+
TargetFolder: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/yaml-reader'
415+
CleanTargetFolder: true
416+
- task: EsrpRelease@14
417+
displayName: 'ESRP Release - YAML reader NuGet'
418+
condition: and(succeeded(), ne(variables['nugetAlreadyPublished'], 'true'))
419+
inputs:
420+
connectedservicename: 'Federated DevX ESRP Managed Identity Connection'
421+
usemanagedidentity: false
422+
keyvaultname: 'akv-prod-eastus'
423+
authcertname: 'ReferenceLibraryPrivateCert'
424+
signcertname: 'ReferencePackagePublisherCertificate'
425+
clientid: '65035b7f-7357-4f29-bf25-c5ee5c3949f8'
426+
intent: 'packagedistribution'
427+
contenttype: 'NuGet'
428+
organizationname: '$(NuGetOrganizationName)'
429+
contentsource: 'Folder'
430+
folderlocation: '$(Pipeline.Workspace)/nuget-packages/$(NuGetOrganizationName)/yaml-reader'
431+
waitforreleasecompletion: true
432+
owners: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com'
433+
approvers: 'christiano@microsoft.com,ramsess@microsoft.com,gavinbarron@microsoft.com,jingjingjia@microsoft.com,peombwa@microsoft.com,treicys@microsoft.com'
434+
serviceendpointurl: 'https://api.esrp.microsoft.com/'
435+
mainpublisher: 'ESRPRELPACMAN'
436+
domaintenantid: 'cdc5aeea-15c5-4db6-b079-fcadd2505dc2'
333437

334438
- deployment: create_github_release
335439
condition: and(contains(variables['build.SourceBranch'], 'refs/tags/v'), succeeded())
@@ -459,7 +563,33 @@ extends:
459563
displayName: 'Get current date'
460564
name: setdate
461565
condition: eq(variables['Build.SourceBranch'], variables['PREVIEW_BRANCH'])
462-
566+
567+
# Keep feed credentials out of the Docker build context and image layers.
568+
- pwsh: |
569+
if ([string]::IsNullOrWhiteSpace($env:FEED_ACCESS_TOKEN)) {
570+
throw "No Azure Artifacts access token available for the Docker build."
571+
}
572+
$feedAccessToken = [System.Security.SecurityElement]::Escape($env:FEED_ACCESS_TOKEN)
573+
@"
574+
<?xml version="1.0" encoding="utf-8"?>
575+
<configuration>
576+
<packageSources>
577+
<clear />
578+
<add key="GraphDeveloperExperiences_Public" value="$(privateFeedBaseUrl)/nuget/v3/index.json" />
579+
</packageSources>
580+
<packageSourceCredentials>
581+
<GraphDeveloperExperiences_Public>
582+
<add key="Username" value="AzureDevOps" />
583+
<add key="ClearTextPassword" value="$feedAccessToken" />
584+
<add key="ValidAuthenticationTypes" value="Basic" />
585+
</GraphDeveloperExperiences_Public>
586+
</packageSourceCredentials>
587+
</configuration>
588+
"@ | Set-Content -Path "$(Agent.TempDirectory)/hidi-docker.nuget.config" -Encoding UTF8
589+
displayName: 'Create Docker NuGet config (central feed)'
590+
env:
591+
FEED_ACCESS_TOKEN: $(System.AccessToken)
592+
463593
- script: |
464594
docker run --privileged --rm msgraphprodregistry.azurecr.io/tonistiigi/binfmt --install all
465595
displayName: "Enable multi-platform builds"
@@ -478,6 +608,7 @@ extends:
478608
# Using quotes around tags to prevent flag interpretation
479609
docker buildx build \
480610
--platform linux/amd64,linux/arm64/v8 \
611+
--secret id=nuget_config,src="$(Agent.TempDirectory)/hidi-docker.nuget.config" \
481612
--push \
482613
-t "$(REGISTRY)/$(IMAGE_NAME):nightly" \
483614
-t "$(REGISTRY)/$(IMAGE_NAME):${VERSION}.${BUILDDATE}${RUNNUMBER}" \
@@ -490,13 +621,22 @@ extends:
490621
echo "Building Docker image for release..."
491622
docker buildx build\
492623
--platform linux/amd64,linux/arm64/v8 \
624+
--secret id=nuget_config,src="$(Agent.TempDirectory)/hidi-docker.nuget.config" \
493625
--push \
494626
-t "$(REGISTRY)/$(IMAGE_NAME):latest" \
495627
-t "$(REGISTRY)/$(IMAGE_NAME):${VERSION}" \
496628
"$(Pipeline.Workspace)"
497629
displayName: 'Build and Push Release Image'
498630
condition: contains(variables['Build.SourceBranch'], 'refs/tags/v')
499631
632+
- pwsh: |
633+
$configPath = "$(Agent.TempDirectory)/hidi-docker.nuget.config"
634+
if (Test-Path $configPath) {
635+
Remove-Item $configPath -Force
636+
}
637+
displayName: 'Remove Docker NuGet config'
638+
condition: always()
639+
500640
# once the nuget has been released, fill this form to get the public documentation updated.
501641
# https://dev.azure.com/msft-skilling/Content/_workitems/create/User%20Story?templateId=39fb91e3-64a2-4c8a-83db-b2bdf3603dd3&ownerId=c4a28f90-17ae-4384-b514-7273392b082b
502642
# https://learn.microsoft.com/en-us/dotnet/api/microsoft.openapi

‎Dockerfile‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ COPY ./src ./hidi/src
55
COPY ./Directory.Build.props ./hidi/Directory.Build.props
66
COPY ./README.md ./hidi/README.md
77
WORKDIR /app/hidi
8-
RUN dotnet publish ./src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj -c Release
8+
# CI supplies the private feed config as a secret; local builds use default NuGet sources.
9+
RUN --mount=type=secret,id=nuget_config,target=/app/hidi/NuGet.Config \
10+
dotnet publish ./src/Microsoft.OpenApi.Hidi/Microsoft.OpenApi.Hidi.csproj -c Release
911

1012
FROM mcr.microsoft.com/dotnet/runtime:8.0-jammy-chiseled AS runtime
1113
WORKDIR /app
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
<#
5+
.SYNOPSIS
6+
Checks whether a NuGet artifact's version exists in an authenticated Azure Artifacts feed.
7+
.DESCRIPTION
8+
Resolves the package content endpoint from the private feed's NuGet v3 service index.
9+
Sets nugetAlreadyPublished for the ESRP release steps; only a missing package or version
10+
permits publishing. Feed authentication and other lookup failures fail the step.
11+
#>
12+
[CmdletBinding()]
13+
param(
14+
[Parameter(Mandatory = $true)]
15+
[string]$PackageDirectory,
16+
[Parameter(Mandatory = $true)]
17+
[string]$PackageId,
18+
[Parameter(Mandatory = $true)]
19+
[string]$NuGetServiceIndexUrl,
20+
[string]$FeedAccessToken = $env:FEED_ACCESS_TOKEN
21+
)
22+
23+
$ErrorActionPreference = 'Stop'
24+
25+
function Assert-PrivateFeedUrl {
26+
param([string]$Url)
27+
28+
$uri = [uri]$Url
29+
if (-not $uri.IsAbsoluteUri -or $uri.Scheme -ne 'https' -or
30+
($uri.Host -ne 'pkgs.dev.azure.com' -and -not $uri.Host.EndsWith('.pkgs.visualstudio.com'))) {
31+
throw "NuGet lookups must use an HTTPS Azure Artifacts feed: $Url"
32+
}
33+
}
34+
35+
Assert-PrivateFeedUrl -Url $NuGetServiceIndexUrl
36+
if ([string]::IsNullOrWhiteSpace($FeedAccessToken)) {
37+
throw 'FEED_ACCESS_TOKEN is required to query the private NuGet feed.'
38+
}
39+
40+
$packagePattern = '^' + [regex]::Escape($PackageId) + '\.(\d[\w\.\-]*)\.nupkg$'
41+
$packages = @(Get-ChildItem -Path $PackageDirectory -File -Filter "$PackageId.*.nupkg" |
42+
Where-Object { $_.Name -match $packagePattern })
43+
if ($packages.Count -ne 1) {
44+
throw "Expected exactly one $PackageId nupkg to publish; found $($packages.Count)."
45+
}
46+
$version = [regex]::Match($packages[0].Name, $packagePattern, 'IgnoreCase').Groups[1].Value
47+
$id = $PackageId.ToLowerInvariant()
48+
$credentials = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("AzureDevOps:$FeedAccessToken"))
49+
$headers = @{
50+
'Authorization' = "Basic $credentials"
51+
'User-Agent' = 'openapi-azdo-pipeline'
52+
}
53+
54+
$index = Invoke-RestMethod -Uri $NuGetServiceIndexUrl -Headers $headers -MaximumRedirection 0
55+
$resource = $index.resources | Where-Object { $_.'@type' -eq 'PackageBaseAddress/3.0.0' } | Select-Object -First 1
56+
if ([string]::IsNullOrWhiteSpace($resource.'@id')) {
57+
throw "No PackageBaseAddress resource found in the NuGet service index at $NuGetServiceIndexUrl"
58+
}
59+
$uri = "$($resource.'@id'.TrimEnd('/'))/$id/index.json"
60+
Assert-PrivateFeedUrl -Url $uri
61+
62+
try {
63+
$response = Invoke-RestMethod -Uri $uri -Headers $headers -MaximumRedirection 0
64+
if ($null -eq $response.versions) {
65+
throw "No versions returned for NuGet $id by the private feed."
66+
}
67+
$alreadyPublished = $response.versions -contains $version
68+
}
69+
catch {
70+
if ([int]$_.Exception.Response.StatusCode -eq 404) {
71+
$alreadyPublished = $false
72+
}
73+
else {
74+
throw
75+
}
76+
}
77+
78+
if ($alreadyPublished) {
79+
Write-Host "NuGet $id $version already present in the private feed; skipping ESRP release (idempotent re-run)."
80+
}
81+
else {
82+
Write-Host "NuGet $id $version not found in the private feed; will publish via ESRP."
83+
}
84+
Write-Host "##vso[task.setvariable variable=nugetAlreadyPublished]$($alreadyPublished.ToString().ToLowerInvariant())"

0 commit comments

Comments
 (0)