Skip to content
Open
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
51 changes: 49 additions & 2 deletions .azure-pipelines/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ parameters:
default: true
- name: Pack
type: boolean
default: false
default: true
- name: Sign
type: boolean
default: true
Expand All @@ -27,7 +27,9 @@ variables:
BuildAgent: ${{ parameters.BuildAgent }}
GitUserEmail: "GraphTooling@service.microsoft.com"
GitUserName: "Microsoft Graph DevX Tooling"
ACR_SERVICE_CONNECTION: 'ACR Images Push Service Connection'
REGISTRY: 'msgraphprodregistry.azurecr.io'
REGISTRY_NAME: 'msgraphprodregistry'
IMAGE_NAME: 'public/microsoftgraph/powershell'

trigger:
Expand Down Expand Up @@ -74,6 +76,32 @@ extends:
artifactName: 'drop'
publishLocation: 'Container'
steps:
- checkout: self
fetchDepth: 2
- task: PowerShell@2
name: DetectAcrChanges
displayName: Detect ACR publishing changes
inputs:
targetType: inline
pwsh: true
script: |
. $(System.DefaultWorkingDirectory)/tools/AcrPipelineHelpers.ps1
$changedPaths = @(git diff --name-only HEAD^1 HEAD)
if ($LASTEXITCODE -ne 0) {
throw "Failed to determine changed paths with exit code $LASTEXITCODE."
}
$shouldPublish = Test-AcrPublishPath -Path $changedPaths
Write-Host "Changed paths:`n$($changedPaths -join "`n")"
Write-Host "##vso[task.setvariable variable=ShouldPublishToAcr;isOutput=true]$($shouldPublish.ToString().ToLowerInvariant())"
- task: PowerShell@2
displayName: Set CI module prerelease version
inputs:
targetType: inline
pwsh: true
script: |
. $(System.DefaultWorkingDirectory)/tools/AcrPipelineHelpers.ps1
$prerelease = Set-CiModulePrerelease -MetadataPath '$(System.DefaultWorkingDirectory)/config/ModuleMetadata.json' -BuildId '$(Build.BuildId)'
Write-Host "Building PowerShell packages with prerelease suffix '$prerelease'."
- script: |
git submodule update --init --recursive
- template: .azure-pipelines/common-templates/install-tools.yml@self
Expand Down Expand Up @@ -105,4 +133,23 @@ extends:
FolderPath: "$(Build.ArtifactStagingDirectory)"
Pattern: "Microsoft.Graph*.nupkg"

- template: .azure-pipelines/common-templates/security-post-checks.yml@self
- template: .azure-pipelines/common-templates/security-post-checks.yml@self
- ${{ if and(eq(parameters.Pack, true), eq(parameters.Sign, true)) }}:
- stage: Deploy_to_ACR
displayName: Deploy PowerShell packages to ACR
dependsOn: stage
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'), ne(variables['Build.Reason'], 'PullRequest'), eq(dependencies.stage.outputs['MsGraphPsSdkCiBuild.DetectAcrChanges.ShouldPublishToAcr'], 'true'))
jobs:
- job: DeployPowerShellPackagesToAcr
displayName: Deploy PowerShell packages to ACR
templateContext:
inputs:
- input: pipelineArtifact
artifactName: drop
targetPath: '$(System.DefaultWorkingDirectory)/drop'
steps:
- template: .azure-pipelines/common-templates/publish-psresources-acr.yml@self
parameters:
AzureSubscription: $(ACR_SERVICE_CONNECTION)
Registry: $(REGISTRY)
RegistryName: $(REGISTRY_NAME)
107 changes: 107 additions & 0 deletions .azure-pipelines/common-templates/publish-psresources-acr.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

parameters:
- name: AzureSubscription
type: string
- name: Registry
type: string
- name: RegistryName
type: string

steps:
- task: AzurePowerShell@5
displayName: 'Publish PowerShell packages to ACR'
inputs:
azureSubscription: ${{ parameters.AzureSubscription }}
ScriptType: InlineScript
azurePowerShellVersion: LatestVersion
pwsh: true
Inline: |
$ErrorActionPreference = 'Stop'
$minimumPsResourceGetVersion = [version]'1.1.1'
$installedModule = Get-Module -ListAvailable -Name Microsoft.PowerShell.PSResourceGet |
Where-Object Version -GE $minimumPsResourceGetVersion |
Sort-Object Version -Descending |
Select-Object -First 1

if ($null -eq $installedModule) {
Install-Module -Name Microsoft.PowerShell.PSResourceGet -MinimumVersion $minimumPsResourceGetVersion -Scope CurrentUser -Force -AllowClobber
}
Import-Module Microsoft.PowerShell.PSResourceGet -MinimumVersion $minimumPsResourceGetVersion -Force

$repositoryName = '${{ parameters.RegistryName }}'
$existingRepository = Get-PSResourceRepository -Name $repositoryName -ErrorAction SilentlyContinue
if ($null -ne $existingRepository) {
Unregister-PSResourceRepository -Name $repositoryName
}
Register-PSResourceRepository -Name $repositoryName -Uri 'https://${{ parameters.Registry }}'

Add-Type -AssemblyName System.IO.Compression.FileSystem
function Get-PackageMetadata {
param([Parameter(Mandatory)][string] $PackagePath)

$archive = [System.IO.Compression.ZipFile]::OpenRead($PackagePath)
try {
$nuspecEntry = $archive.Entries | Where-Object FullName -Like '*.nuspec' | Select-Object -First 1
if ($null -eq $nuspecEntry) {
throw "Package '$PackagePath' doesn't contain a nuspec file."
}

$reader = [System.IO.StreamReader]::new($nuspecEntry.Open())
try {
[xml] $nuspec = $reader.ReadToEnd()
}
finally {
$reader.Dispose()
}

return [pscustomobject]@{
Id = [string]$nuspec.package.metadata.id
Version = [string]$nuspec.package.metadata.version
Path = $PackagePath
}
}
finally {
$archive.Dispose()
}
}

$artifactPath = '$(System.DefaultWorkingDirectory)/drop'
$packages = @(Get-ChildItem -Path $artifactPath -Recurse -File -Filter 'Microsoft.Graph*.nupkg' |
ForEach-Object { Get-PackageMetadata -PackagePath $_.FullName })
if ($packages.Count -eq 0) {
throw "No Microsoft Graph PowerShell packages were found under '$artifactPath'."
}

$packages = $packages | Sort-Object @{
Expression = {
if ($_.Id -eq 'Microsoft.Graph.Authentication') { 0 }
elseif ($_.Id -in @('Microsoft.Graph', 'Microsoft.Graph.Beta')) { 2 }
else { 1 }
}
}, Id

foreach ($package in $packages) {
$existingPackage = Find-PSResource -Name $package.Id -Version $package.Version -Repository $repositoryName -ErrorAction SilentlyContinue
if ($null -ne $existingPackage) {
Write-Host "$($package.Id) $($package.Version) already exists in $repositoryName; continuing the idempotent deployment."
continue
}

Write-Host "Publishing $($package.Id) $($package.Version) to $repositoryName."
Publish-PSResource -NupkgPath $package.Path -Repository $repositoryName -ErrorAction Stop
}

foreach ($package in $packages) {
$foundPackage = $null
for ($attempt = 1; $attempt -le 6 -and $null -eq $foundPackage; $attempt++) {
$foundPackage = Find-PSResource -Name $package.Id -Version $package.Version -Repository $repositoryName -ErrorAction SilentlyContinue
if ($null -eq $foundPackage -and $attempt -lt 6) {
Start-Sleep -Seconds 10
}
}
if ($null -eq $foundPackage) {
throw "Published package '$($package.Id)' version '$($package.Version)' couldn't be found in ACR."
}
}
22 changes: 22 additions & 0 deletions .azure-pipelines/sdk-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ variables:
BuildAgent: ${{ parameters.BuildAgent }}
GitUserEmail: "GraphTooling@service.microsoft.com"
GitUserName: "Microsoft Graph DevX Tooling"
ACR_SERVICE_CONNECTION: 'ACR Images Push Service Connection'
REGISTRY: 'msgraphprodregistry.azurecr.io'
REGISTRY_NAME: 'msgraphprodregistry'
IMAGE_NAME: 'public/microsoftgraph/powershell'
PREVIEW_BRANCH: 'refs/heads/main' # Updated to target your branch

Expand Down Expand Up @@ -158,6 +160,26 @@ extends:
nuGetFeedType: external
publishFeedCredentials: 'microsoftgraph PowerShell Gallery connection'

- ${{ if and(eq(parameters.Pack, true), eq(parameters.Sign, true)) }}:
- stage: Deploy_to_ACR
displayName: Deploy PowerShell packages to ACR
dependsOn: stage
condition: succeeded()
jobs:
- job: DeployPowerShellPackagesToAcr
displayName: Deploy PowerShell packages to ACR
templateContext:
inputs:
- input: pipelineArtifact
artifactName: drop
targetPath: '$(System.DefaultWorkingDirectory)/drop'
steps:
- template: .azure-pipelines/common-templates/publish-psresources-acr.yml@self
parameters:
AzureSubscription: $(ACR_SERVICE_CONNECTION)
Registry: $(REGISTRY)
RegistryName: $(REGISTRY_NAME)

- stage: PushDockerImageToRegistry
condition: and(or(startsWith(variables['Build.SourceBranch'], 'refs/tags/v'), eq(variables['Build.SourceBranch'], variables['PREVIEW_BRANCH'])), not(contains(variables['Build.SourceBranch'], '-preview')))
dependsOn: stage
Expand Down
57 changes: 57 additions & 0 deletions tools/AcrPipelineHelpers.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

function Set-CiModulePrerelease {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[string] $MetadataPath,

[Parameter(Mandatory)]
[ValidatePattern('^\d+$')]
[string] $BuildId
)

$metadata = Get-Content -Path $MetadataPath -Raw | ConvertFrom-Json -AsHashtable
$prerelease = "ci$BuildId"

foreach ($versionMetadata in $metadata.versions.Values) {
$versionMetadata.prerelease = $prerelease
}

$metadata | ConvertTo-Json -Depth 100 | Set-Content -Path $MetadataPath -Encoding utf8
return $prerelease
}

function Test-AcrPublishPath {
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[AllowEmptyString()]
[string[]] $Path
)

begin {
$isRelevant = $false
$patterns = @(
'^src/Authentication(?:/|$)',
'^config(?:/|$)',
'^autorest\.powershell(?:/|$)',
'^openApiDocs(?:/|$)'
)
}

process {
foreach ($item in $Path) {
$normalizedPath = $item.Replace('\', '/')
if ($patterns.Where({ $normalizedPath -match $_ }, 'First').Count -gt 0) {
$isRelevant = $true
}
}
}

end {
return $isRelevant
}
}
57 changes: 57 additions & 0 deletions tools/Tests/AcrPipelineHelpers.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

BeforeAll {
. (Join-Path $PSScriptRoot '..\AcrPipelineHelpers.ps1')
}

Describe 'Set-CiModulePrerelease' {
It 'sets every module prerelease value to the build-specific suffix' {
$metadataPath = Join-Path $TestDrive 'ModuleMetadata.json'
@{
versions = @{
authentication = @{ version = '2.39.0'; prerelease = '' }
beta = @{ version = '2.39.0'; prerelease = '' }
'v1.0' = @{ version = '2.39.0'; prerelease = '' }
}
} | ConvertTo-Json -Depth 10 | Set-Content -Path $metadataPath

$prerelease = Set-CiModulePrerelease -MetadataPath $metadataPath -BuildId '12345'
$prerelease | Should -Be 'ci12345'

$metadata = Get-Content -Path $metadataPath -Raw | ConvertFrom-Json
$metadata.versions.authentication.prerelease | Should -Be 'ci12345'
$metadata.versions.beta.prerelease | Should -Be 'ci12345'
$metadata.versions.'v1.0'.prerelease | Should -Be 'ci12345'

$manifestPath = Join-Path $TestDrive 'TestModule.psd1'
Set-Content -Path (Join-Path $TestDrive 'TestModule.psm1') -Value ''
New-ModuleManifest -Path $manifestPath -RootModule 'TestModule.psm1' -ModuleVersion '1.0.0'
{ Update-ModuleManifest -Path $manifestPath -Prerelease $prerelease } | Should -Not -Throw
}
}

Describe 'Test-AcrPublishPath' {
It 'matches relevant source and generation inputs' -ForEach @(
'src/Authentication/Authentication/Microsoft.Graph.Authentication.psd1'
'SRC\AUTHENTICATION\Authentication.Core\Authentication.cs'
'config/ModuleMetadata.json'
'autorest.powershell/packages/autorest.powershell/package.json'
'openApiDocs/v1.0/Users.yml'
) {
Test-AcrPublishPath -Path $_ | Should -BeTrue
}

It 'does not match unrelated paths' -ForEach @(
'docs/readme.md'
'samples/1-Users.ps1'
'src/Users/v1.0/readme.md'
'.azure-pipelines/ci-build.yml'
) {
Test-AcrPublishPath -Path $_ | Should -BeFalse
}

It 'returns true when any changed path is relevant' {
Test-AcrPublishPath -Path @('docs/readme.md', 'config/ModulesMapping.jsonc') | Should -BeTrue
}
}
Loading