Skip to content

Commit bd5ab3d

Browse files
Microsoft Graph DevX ToolingMicrosoft Graph DevX Tooling
authored andcommitted
feat: publish PowerShell packages to ACR
Add reusable PSResourceGet publishing for signed CI and release packages, with build-specific CI prerelease versions and path-based deployment gating. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: af247b04-ecf8-4873-a78c-33d6437bb0d0
1 parent ffb89f7 commit bd5ab3d

5 files changed

Lines changed: 291 additions & 2 deletions

File tree

.azure-pipelines/ci-build.yml

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ parameters:
1010
default: true
1111
- name: Pack
1212
type: boolean
13-
default: false
13+
default: true
1414
- name: Sign
1515
type: boolean
1616
default: true
@@ -74,6 +74,32 @@ extends:
7474
artifactName: 'drop'
7575
publishLocation: 'Container'
7676
steps:
77+
- checkout: self
78+
fetchDepth: 2
79+
- task: PowerShell@2
80+
name: DetectAcrChanges
81+
displayName: Detect ACR publishing changes
82+
inputs:
83+
targetType: inline
84+
pwsh: true
85+
script: |
86+
. $(System.DefaultWorkingDirectory)/tools/AcrPipelineHelpers.ps1
87+
$changedPaths = @(git diff --name-only HEAD^1 HEAD)
88+
if ($LASTEXITCODE -ne 0) {
89+
throw "Failed to determine changed paths with exit code $LASTEXITCODE."
90+
}
91+
$shouldPublish = Test-AcrPublishPath -Path $changedPaths
92+
Write-Host "Changed paths:`n$($changedPaths -join "`n")"
93+
Write-Host "##vso[task.setvariable variable=ShouldPublishToAcr;isOutput=true]$($shouldPublish.ToString().ToLowerInvariant())"
94+
- task: PowerShell@2
95+
displayName: Set CI module prerelease version
96+
inputs:
97+
targetType: inline
98+
pwsh: true
99+
script: |
100+
. $(System.DefaultWorkingDirectory)/tools/AcrPipelineHelpers.ps1
101+
$prerelease = Set-CiModulePrerelease -MetadataPath '$(System.DefaultWorkingDirectory)/config/ModuleMetadata.json' -BuildId '$(Build.BuildId)'
102+
Write-Host "Building PowerShell packages with prerelease suffix '$prerelease'."
77103
- script: |
78104
git submodule update --init --recursive
79105
- template: .azure-pipelines/common-templates/install-tools.yml@self
@@ -105,4 +131,25 @@ extends:
105131
FolderPath: "$(Build.ArtifactStagingDirectory)"
106132
Pattern: "Microsoft.Graph*.nupkg"
107133

108-
- template: .azure-pipelines/common-templates/security-post-checks.yml@self
134+
- template: .azure-pipelines/common-templates/security-post-checks.yml@self
135+
- ${{ if and(eq(parameters.Pack, true), eq(parameters.Sign, true)) }}:
136+
- stage: Deploy_to_ACR
137+
displayName: Deploy PowerShell packages to ACR
138+
dependsOn: stage
139+
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'), ne(variables['Build.Reason'], 'PullRequest'), eq(dependencies.stage.outputs['MsGraphPsSdkCiBuild.DetectAcrChanges.ShouldPublishToAcr'], 'true'))
140+
jobs:
141+
- deployment: DeployPowerShellPackagesToAcr
142+
displayName: Deploy PowerShell packages to ACR
143+
environment: PowerShellAcr
144+
templateContext:
145+
type: releaseJob
146+
isProduction: true
147+
inputs:
148+
- input: pipelineArtifact
149+
artifactName: drop
150+
targetPath: '$(System.DefaultWorkingDirectory)/drop'
151+
strategy:
152+
runOnce:
153+
deploy:
154+
steps:
155+
- template: .azure-pipelines/common-templates/publish-psresources-acr.yml@self
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
parameters:
5+
- name: AzureSubscription
6+
type: string
7+
default: 'ACR Images Push Service Connection'
8+
- name: RegistryUri
9+
type: string
10+
default: 'https://msgraphprodregistry.azurecr.io'
11+
- name: RepositoryName
12+
type: string
13+
default: 'MicrosoftGraphAcr'
14+
- name: ArtifactPath
15+
type: string
16+
default: '$(System.DefaultWorkingDirectory)/drop'
17+
18+
steps:
19+
- task: AzurePowerShell@5
20+
displayName: 'Publish PowerShell packages to ACR'
21+
inputs:
22+
azureSubscription: ${{ parameters.AzureSubscription }}
23+
ScriptType: InlineScript
24+
azurePowerShellVersion: LatestVersion
25+
pwsh: true
26+
Inline: |
27+
$ErrorActionPreference = 'Stop'
28+
$minimumPsResourceGetVersion = [version]'1.1.1'
29+
$installedModule = Get-Module -ListAvailable -Name Microsoft.PowerShell.PSResourceGet |
30+
Where-Object Version -GE $minimumPsResourceGetVersion |
31+
Sort-Object Version -Descending |
32+
Select-Object -First 1
33+
34+
if ($null -eq $installedModule) {
35+
Install-Module -Name Microsoft.PowerShell.PSResourceGet -MinimumVersion $minimumPsResourceGetVersion -Scope CurrentUser -Force -AllowClobber
36+
}
37+
Import-Module Microsoft.PowerShell.PSResourceGet -MinimumVersion $minimumPsResourceGetVersion -Force
38+
39+
$repositoryName = '${{ parameters.RepositoryName }}'
40+
$existingRepository = Get-PSResourceRepository -Name $repositoryName -ErrorAction SilentlyContinue
41+
if ($null -ne $existingRepository) {
42+
Unregister-PSResourceRepository -Name $repositoryName
43+
}
44+
Register-PSResourceRepository -Name $repositoryName -Uri '${{ parameters.RegistryUri }}'
45+
46+
Add-Type -AssemblyName System.IO.Compression.FileSystem
47+
function Get-PackageMetadata {
48+
param([Parameter(Mandatory)][string] $PackagePath)
49+
50+
$archive = [System.IO.Compression.ZipFile]::OpenRead($PackagePath)
51+
try {
52+
$nuspecEntry = $archive.Entries | Where-Object FullName -Like '*.nuspec' | Select-Object -First 1
53+
if ($null -eq $nuspecEntry) {
54+
throw "Package '$PackagePath' doesn't contain a nuspec file."
55+
}
56+
57+
$reader = [System.IO.StreamReader]::new($nuspecEntry.Open())
58+
try {
59+
[xml] $nuspec = $reader.ReadToEnd()
60+
}
61+
finally {
62+
$reader.Dispose()
63+
}
64+
65+
return [pscustomobject]@{
66+
Id = [string]$nuspec.package.metadata.id
67+
Version = [string]$nuspec.package.metadata.version
68+
Path = $PackagePath
69+
}
70+
}
71+
finally {
72+
$archive.Dispose()
73+
}
74+
}
75+
76+
$packages = @(Get-ChildItem -Path '${{ parameters.ArtifactPath }}' -Recurse -File -Filter 'Microsoft.Graph*.nupkg' |
77+
ForEach-Object { Get-PackageMetadata -PackagePath $_.FullName })
78+
if ($packages.Count -eq 0) {
79+
throw "No Microsoft Graph PowerShell packages were found under '${{ parameters.ArtifactPath }}'."
80+
}
81+
82+
$packages = $packages | Sort-Object @{
83+
Expression = {
84+
if ($_.Id -eq 'Microsoft.Graph.Authentication') { 0 }
85+
elseif ($_.Id -in @('Microsoft.Graph', 'Microsoft.Graph.Beta')) { 2 }
86+
else { 1 }
87+
}
88+
}, Id
89+
90+
foreach ($package in $packages) {
91+
$existingPackage = Find-PSResource -Name $package.Id -Version $package.Version -Repository $repositoryName -ErrorAction SilentlyContinue
92+
if ($null -ne $existingPackage) {
93+
Write-Host "$($package.Id) $($package.Version) already exists in $repositoryName; continuing the idempotent deployment."
94+
continue
95+
}
96+
97+
Write-Host "Publishing $($package.Id) $($package.Version) to $repositoryName."
98+
Publish-PSResource -NupkgPath $package.Path -Repository $repositoryName -ErrorAction Stop
99+
}
100+
101+
foreach ($package in $packages) {
102+
$foundPackage = $null
103+
for ($attempt = 1; $attempt -le 6 -and $null -eq $foundPackage; $attempt++) {
104+
$foundPackage = Find-PSResource -Name $package.Id -Version $package.Version -Repository $repositoryName -ErrorAction SilentlyContinue
105+
if ($null -eq $foundPackage -and $attempt -lt 6) {
106+
Start-Sleep -Seconds 10
107+
}
108+
}
109+
if ($null -eq $foundPackage) {
110+
throw "Published package '$($package.Id)' version '$($package.Version)' couldn't be found in ACR."
111+
}
112+
}

.azure-pipelines/sdk-release.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,28 @@ extends:
158158
nuGetFeedType: external
159159
publishFeedCredentials: 'microsoftgraph PowerShell Gallery connection'
160160

161+
- ${{ if and(eq(parameters.Pack, true), eq(parameters.Sign, true)) }}:
162+
- stage: Deploy_to_ACR
163+
displayName: Deploy PowerShell packages to ACR
164+
dependsOn: stage
165+
condition: succeeded()
166+
jobs:
167+
- deployment: DeployPowerShellPackagesToAcr
168+
displayName: Deploy PowerShell packages to ACR
169+
environment: PowerShellAcr
170+
templateContext:
171+
type: releaseJob
172+
isProduction: true
173+
inputs:
174+
- input: pipelineArtifact
175+
artifactName: drop
176+
targetPath: '$(System.DefaultWorkingDirectory)/drop'
177+
strategy:
178+
runOnce:
179+
deploy:
180+
steps:
181+
- template: .azure-pipelines/common-templates/publish-psresources-acr.yml@self
182+
161183
- stage: PushDockerImageToRegistry
162184
condition: and(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), eq(variables['Build.SourceBranch'], variables['PREVIEW_BRANCH'])), not(contains(variables['Build.SourceBranch'], '-preview')))
163185
dependsOn: stage

tools/AcrPipelineHelpers.ps1

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
function Set-CiModulePrerelease {
5+
[CmdletBinding()]
6+
param(
7+
[Parameter(Mandatory)]
8+
[ValidateNotNullOrEmpty()]
9+
[string] $MetadataPath,
10+
11+
[Parameter(Mandatory)]
12+
[ValidatePattern('^\d+$')]
13+
[string] $BuildId
14+
)
15+
16+
$metadata = Get-Content -Path $MetadataPath -Raw | ConvertFrom-Json -AsHashtable
17+
$prerelease = "ci.$BuildId"
18+
19+
foreach ($versionMetadata in $metadata.versions.Values) {
20+
$versionMetadata.prerelease = $prerelease
21+
}
22+
23+
$metadata | ConvertTo-Json -Depth 100 | Set-Content -Path $MetadataPath -Encoding utf8
24+
return $prerelease
25+
}
26+
27+
function Test-AcrPublishPath {
28+
[CmdletBinding()]
29+
param(
30+
[Parameter(Mandatory, ValueFromPipeline)]
31+
[AllowEmptyString()]
32+
[string[]] $Path
33+
)
34+
35+
begin {
36+
$isRelevant = $false
37+
$patterns = @(
38+
'^src/Authentication(?:/|$)',
39+
'^config(?:/|$)',
40+
'^autorest\.powershell(?:/|$)',
41+
'^openApiDocs(?:/|$)'
42+
)
43+
}
44+
45+
process {
46+
foreach ($item in $Path) {
47+
$normalizedPath = $item.Replace('\', '/')
48+
if ($patterns.Where({ $normalizedPath -match $_ }, 'First').Count -gt 0) {
49+
$isRelevant = $true
50+
}
51+
}
52+
}
53+
54+
end {
55+
return $isRelevant
56+
}
57+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Copyright (c) Microsoft Corporation. All rights reserved.
2+
# Licensed under the MIT License.
3+
4+
BeforeAll {
5+
. (Join-Path $PSScriptRoot '..\AcrPipelineHelpers.ps1')
6+
}
7+
8+
Describe 'Set-CiModulePrerelease' {
9+
It 'sets every module prerelease value to the build-specific suffix' {
10+
$metadataPath = Join-Path $TestDrive 'ModuleMetadata.json'
11+
@{
12+
versions = @{
13+
authentication = @{ version = '2.39.0'; prerelease = '' }
14+
beta = @{ version = '2.39.0'; prerelease = '' }
15+
'v1.0' = @{ version = '2.39.0'; prerelease = '' }
16+
}
17+
} | ConvertTo-Json -Depth 10 | Set-Content -Path $metadataPath
18+
19+
Set-CiModulePrerelease -MetadataPath $metadataPath -BuildId '12345' | Should -Be 'ci.12345'
20+
21+
$metadata = Get-Content -Path $metadataPath -Raw | ConvertFrom-Json
22+
$metadata.versions.authentication.prerelease | Should -Be 'ci.12345'
23+
$metadata.versions.beta.prerelease | Should -Be 'ci.12345'
24+
$metadata.versions.'v1.0'.prerelease | Should -Be 'ci.12345'
25+
}
26+
}
27+
28+
Describe 'Test-AcrPublishPath' {
29+
It 'matches relevant source and generation inputs' -ForEach @(
30+
'src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1'
31+
'SRC\AUTHENTICATION\Authentication.Core\Authentication.cs'
32+
'config/ModuleMetadata.json'
33+
'autorest.powershell/packages/autorest.powershell/package.json'
34+
'openApiDocs/v1.0/Users.yml'
35+
) {
36+
Test-AcrPublishPath -Path $_ | Should -BeTrue
37+
}
38+
39+
It 'does not match unrelated paths' -ForEach @(
40+
'docs/readme.md'
41+
'samples/1-Users.ps1'
42+
'src/Users/v1.0/readme.md'
43+
'.azure-pipelines/ci-build.yml'
44+
) {
45+
Test-AcrPublishPath -Path $_ | Should -BeFalse
46+
}
47+
48+
It 'returns true when any changed path is relevant' {
49+
Test-AcrPublishPath -Path @('docs/readme.md', 'config/ModulesMapping.jsonc') | Should -BeTrue
50+
}
51+
}

0 commit comments

Comments
 (0)