Azure Pipelines
Azure Pipelines is a cloud-based CI/CD service that's part of Azure DevOps. This guide shows you how to run psake builds in Azure Pipelines, including multi-stage pipelines, secret management with variable groups, and publishing to Azure Artifacts.
Quick Start
Here's a basic Azure Pipeline that runs a psake build:
# azure-pipelines.yml
trigger:
branches:
include:
- main
- develop
pool:
vmImage: 'windows-latest'
steps:
- pwsh: |
Install-Module -Name psake -Scope CurrentUser -Force
Invoke-psake -buildFile .\psakefile.ps1 -taskList Build
displayName: 'Run psake build'
Installing psake in Azure Pipelines
Option 1: Install from PowerShell Gallery (Recommended)
- pwsh: |
Install-Module -Name psake -Scope CurrentUser -Force
displayName: 'Install psake'
This works on all agent types (Windows, Linux, macOS).
Option 2: Install Specific Version
- pwsh: |
Install-Module -Name psake -RequiredVersion 4.9.0 -Scope CurrentUser -Force
displayName: 'Install psake 4.9.0'
Option 3: Using requirements.psd1 with PSDepend
If your project uses a requirements.psd1 file:
- pwsh: |
Install-Module -Name PSDepend -Scope CurrentUser -Force
Invoke-PSDepend -Path ./requirements.psd1 -Install -Force
displayName: 'Install dependencies with PSDepend'
Your requirements.psd1:
@{
psake = @{
Version = '4.9.0'
}
Pester = @{
Version = '5.5.0'
}
}
Option 4: Cache PowerShell Modules
Speed up builds by caching the PowerShell modules directory:
- task: Cache@2
inputs:
key: 'psake | "$(Agent.OS)" | requirements.psd1'
restoreKeys: |
psake | "$(Agent.OS)"
psake
path: $(Pipeline.Workspace)/.psmodules
displayName: 'Cache PowerShell modules'
- pwsh: |
$modulePath = "$(Pipeline.Workspace)/.psmodules"
if (-not (Test-Path $modulePath)) {
New-Item -ItemType Directory -Path $modulePath -Force | Out-Null
}
if ($env:PSModulePath -notlike "*$modulePath*") {
$env:PSModulePath = "$modulePath$([System.IO.Path]::PathSeparator)$env:PSModulePath"
}
if (-not (Get-Module -ListAvailable -Name psake)) {
Save-Module -Name psake -Path $modulePath -Force
}
Import-Module psake
displayName: 'Setup psake with caching'
Complete Pipeline Example
Here's a comprehensive pipeline demonstrating psake integration:
# azure-pipelines.yml
trigger:
branches:
include:
- main
- develop
tags:
include:
- v*
pr:
branches:
include:
- main
variables:
buildConfiguration: 'Release'
pool:
vmImage: 'windows-latest'
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: BuildJob
displayName: 'Build with psake'
steps:
- checkout: self
fetchDepth: 0 # Full history for versioning
displayName: 'Checkout source code'
- task: Cache@2
inputs:
key: 'psmodules | "$(Agent.OS)" | requirements.psd1'
path: $(Pipeline.Workspace)/.psmodules
displayName: 'Cache PowerShell modules'
- pwsh: |
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted
Install-Module -Name psake -Scope CurrentUser -Force
Install-Module -Name PSDepend -Scope CurrentUser -Force
if (Test-Path ./requirements.psd1) {
Invoke-PSDepend -Path ./requirements.psd1 -Install -Force
}
displayName: 'Install psake and dependencies'
- pwsh: |
Invoke-psake -buildFile .\psakefile.ps1 `
-taskList Build, Test `
-parameters @{
Configuration = "$(buildConfiguration)"
BuildNumber = "$(Build.BuildNumber)"
BranchName = "$(Build.SourceBranchName)"
}
displayName: 'Run psake build and test'
env:
NUGET_API_KEY: $(NuGetApiKey) # From variable group
- task: PublishTestResults@2
condition: succeededOrFailed()
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResults/*.xml'
mergeTestResults: true
testRunTitle: 'Unit Tests'
displayName: 'Publish test results'
- task: PublishCodeCoverageResults@2
condition: succeededOrFailed()
inputs:
codeCoverageTool: 'JaCoCo'
summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml'
displayName: 'Publish code coverage'
- publish: $(System.DefaultWorkingDirectory)/build
artifact: BuildOutput
displayName: 'Publish build artifacts'
- stage: Deploy
displayName: 'Deploy to Production'
dependsOn: Build
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployJob
displayName: 'Deploy with psake'
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: BuildOutput
displayName: 'Download build artifacts'
- pwsh: |
Install-Module -Name psake -Scope CurrentUser -Force
displayName: 'Install psake'
- pwsh: |
Invoke-psake -buildFile .\psakefile.ps1 `
-taskList Deploy `
-parameters @{
Environment = 'Production'
DeploymentPath = "$(Pipeline.Workspace)/BuildOutput"
}
displayName: 'Run psake deployment'
env:
AZURE_CONNECTION_STRING: $(AzureConnectionString)
Multi-Stage Pipelines
Azure Pipelines supports multi-stage pipelines for complex workflows:
stages:
- stage: Build
jobs:
- job: CompileAndTest
steps:
- pwsh: |
Invoke-psake -taskList Build, Test
displayName: 'Build and Test'
- stage: QA
dependsOn: Build
condition: succeeded()
jobs:
- job: DeployToQA
steps:
- pwsh: |
Invoke-psake -taskList Deploy -parameters @{ Environment = 'QA' }
displayName: 'Deploy to QA'
- stage: Production
dependsOn: QA
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployToProduction
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- pwsh: |
Invoke-psake -taskList Deploy -parameters @{ Environment = 'Production' }
displayName: 'Deploy to Production'
Cross-Platform Matrix Builds
Run psake builds across multiple operating systems and PowerShell versions:
trigger:
- main
strategy:
matrix:
Windows_PS7:
imageName: 'windows-latest'
pwshVersion: '7.4'
Linux_PS7:
imageName: 'ubuntu-latest'
pwshVersion: '7.4'
macOS_PS7:
imageName: 'macOS-latest'
pwshVersion: '7.4'
Windows_PS51:
imageName: 'windows-2019'
pwshVersion: '5.1'
pool:
vmImage: $(imageName)
steps:
- pwsh: |
Write-Host "OS: $(imageName)"
Write-Host "PowerShell Version: $($PSVersionTable.PSVersion)"
displayName: 'Display environment info'
- pwsh: |
Install-Module -Name psake -Scope CurrentUser -Force
displayName: 'Install psake'
- pwsh: |
Invoke-psake -buildFile .\psakefile.ps1 -taskList Build, Test
displayName: 'Run psake build'
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: '$(System.DefaultWorkingDirectory)/build'
artifactName: 'build-$(imageName)-ps$(pwshVersion)'
displayName: 'Upload artifacts'
Variable Groups and Secrets
Azure Pipelines provides variable groups for managing secrets and configuration.
Creating Variable Groups
- Go to Pipelines → Library in Azure DevOps
- Click + Variable group
- Name your group (e.g.,
BuildSecrets) - Add variables:
NuGetApiKey(click lock icon to mark as secret)AzureConnectionString(secret)Environment(plain text)
- Save the variable group
Using Variable Groups in Pipelines
variables:
- group: BuildSecrets # Reference the variable group
- name: buildConfiguration
value: 'Release'
steps:
- pwsh: |
Invoke-psake -buildFile .\psakefile.ps1 `
-taskList Deploy `
-parameters @{
NuGetApiKey = $env:NUGET_API_KEY
Environment = $env:ENVIRONMENT
}
displayName: 'Deploy with secrets'
env:
NUGET_API_KEY: $(NuGetApiKey)
ENVIRONMENT: $(Environment)
AZURE_CONNECTION_STRING: $(AzureConnectionString)
Using Azure Key Vault
For enhanced security, integrate with Azure Key Vault:
variables:
- group: BuildSecrets
- name: KeyVaultName
value: 'my-keyvault'
steps:
- task: AzureKeyVault@2
inputs:
azureSubscription: 'Azure Subscription Connection'
KeyVaultName: '$(KeyVaultName)'
SecretsFilter: '*'
RunAsPreJob: true
displayName: 'Fetch secrets from Key Vault'
- pwsh: |
# Secrets are now available as environment variables
Invoke-psake -taskList Deploy
displayName: 'Deploy with Key Vault secrets'
env:
NUGET_API_KEY: $(NuGetApiKey) # From Key Vault
Security Best Practices
- Mark secrets as secret in variable groups (use the lock icon)
- Use Azure Key Vault for production secrets
- Limit access with variable group permissions
- Use service connections for Azure/AWS credentials instead of manual secrets
- Enable Azure DevOps auditing to track secret access
Publishing to Azure Artifacts
Publishing NuGet Packages
steps:
- pwsh: |
Invoke-psake -taskList Build, Pack
displayName: 'Build and pack NuGet packages'
- task: NuGetCommand@2
inputs:
command: 'push'
packagesToPush: '$(Build.SourcesDirectory)/build/*.nupkg'
nuGetFeedType: 'internal'
publishVstsFeed: 'MyProject/MyFeed'
displayName: 'Publish to Azure Artifacts feed'
Publishing PowerShell Modules
steps:
- pwsh: |
Invoke-psake -taskList Build, Test
displayName: 'Build PowerShell module'
- task: PowerShell@2
inputs:
targetType: 'inline'
script: |
$apiKey = $env:ARTIFACTS_PAT
Register-PSRepository -Name AzureArtifacts `
-SourceLocation "https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v2" `
-PublishLocation "https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v2" `
-InstallationPolicy Trusted
Publish-Module -Path ./build/MyModule -Repository AzureArtifacts -NuGetApiKey $apiKey
displayName: 'Publish module to Azure Artifacts'
env:
ARTIFACTS_PAT: $(System.AccessToken)
Publishing Universal Packages
steps:
- pwsh: |
Invoke-psake -taskList Build
displayName: 'Build application'
- task: UniversalPackages@0
inputs:
command: 'publish'
publishDirectory: '$(Build.SourcesDirectory)/build'
feedsToUsePublish: 'internal'
vstsFeedPublish: 'MyProject/MyFeed'
vstsFeedPackagePublish: 'myapp'
versionOption: 'patch'
displayName: 'Publish Universal Package'
Example psakefile.ps1 for Azure Pipelines
Properties {
$Configuration = 'Debug'
$BuildNumber = '0'
$BranchName = 'unknown'
$Version = "1.0.$BuildNumber"
$SrcDir = Join-Path $PSScriptRoot 'src'
$TestDir = Join-Path $PSScriptRoot 'tests'
$BuildDir = Join-Path $PSScriptRoot 'build'
$Environment = 'Development'
}
Task Default -depends Build, Test
Task Clean {
if (Test-Path $BuildDir) {
Remove-Item $BuildDir -Recurse -Force
}
New-Item -ItemType Directory -Path $BuildDir | Out-Null
Write-Host "Build directory cleaned: $BuildDir" -ForegroundColor Green
}
Task Build -depends Clean {
Write-Host "Building version $Version from branch $BranchName" -ForegroundColor Cyan
exec {
dotnet build $SrcDir `
-c $Configuration `
-o $BuildDir `
/p:Version=$Version `
/p:AssemblyVersion=$Version
}
}
Task Test -depends Build {
Write-Host "Running tests..." -ForegroundColor Cyan
exec {
dotnet test $TestDir `
-c $Configuration `
--no-build `
--logger "trx;LogFileName=TestResults.xml" `
--results-directory "$BuildDir/TestResults" `
/p:CollectCoverage=true `
/p:CoverletOutputFormat=cobertura `
/p:CoverletOutput="$BuildDir/coverage.xml"
}
}
Task Pack -depends Build {
Write-Host "Creating NuGet packages..." -ForegroundColor Cyan
exec {
dotnet pack $SrcDir `
-c $Configuration `
-o $BuildDir `
--no-build `
/p:Version=$Version
}
}
Task Deploy -depends Pack {
$apiKey = $env:NUGET_API_KEY
if ([string]::IsNullOrEmpty($apiKey)) {
throw "NUGET_API_KEY environment variable is required for deployment"
}
Write-Host "Deploying to $Environment environment..." -ForegroundColor Cyan
if ($Environment -eq 'Production') {
# Deploy to NuGet.org
Get-ChildItem "$BuildDir/*.nupkg" | ForEach-Object {
exec {
dotnet nuget push $_.FullName `
--api-key $apiKey `
--source https://api.nuget.org/v3/index.json
}
}
}
else {
Write-Host "Skipping deployment for non-production environment: $Environment"
}
}
Task Publish -depends Deploy {
Write-Host "Publishing artifacts to Azure Artifacts..." -ForegroundColor Cyan
# Additional publishing logic here
}
Common Troubleshooting
psake Module Not Found on Agent
Problem: Import-Module: The specified module 'psake' was not loaded
Solution: Ensure you're using pwsh (PowerShell Core) and install with -Scope CurrentUser:
- pwsh: |
Install-Module -Name psake -Scope CurrentUser -Force -Verbose
Get-Module -ListAvailable psake
displayName: 'Install psake with verbose output'
Build Fails but Pipeline Shows Success
Problem: psake build fails but Azure Pipeline doesn't detect the failure
Solution: Use the exec function in psake for external commands:
Task Build {
# This will fail the build on non-zero exit codes
exec { dotnet build }
}
Or explicitly check for errors:
- pwsh: |
Invoke-psake -buildFile .\psakefile.ps1
if ($LASTEXITCODE -ne 0) {
Write-Error "psake build failed"
exit $LASTEXITCODE
}
displayName: 'Run psake with error checking'
Variable Group Not Available
Problem: Variables from variable group are empty
Solution: Reference the variable group at the pipeline or stage level:
# At pipeline level
variables:
- group: BuildSecrets
# Or at stage level
stages:
- stage: Build
variables:
- group: BuildSecrets
Secrets Not Passed to Child Processes
Problem: Environment variables with secrets aren't available in psake
Solution: Explicitly pass them using the env parameter:
- pwsh: |
Invoke-psake -taskList Deploy
env:
NUGET_API_KEY: $(NuGetApiKey) # Explicitly map to environment variable
displayName: 'Deploy with secrets'
Agent Pool Capacity Issues
Problem: Builds queued for a long time waiting for agents
Solution: Use Microsoft-hosted agents or scale your self-hosted agent pool:
pool:
vmImage: 'windows-latest' # Microsoft-hosted agent
# Or for self-hosted:
# name: 'Default'
# demands:
# - Agent.OS -equals Windows_NT
Path Issues with Self-Hosted Agents
Problem: Paths don't resolve correctly on self-hosted agents
Solution: Use PowerShell's built-in path cmdlets:
Properties {
# Use cross-platform path construction
$BuildDir = Join-Path $PSScriptRoot 'build'
$OutputPath = Join-Path $BuildDir 'bin'
}
Advanced Patterns
Parameterized Builds with Runtime Parameters
parameters:
- name: buildConfiguration
displayName: 'Build Configuration'
type: string
default: 'Release'
values:
- Debug
- Release
- name: taskList
displayName: 'psake Tasks'
type: string
default: 'Build, Test'
steps:
- pwsh: |
Invoke-psake -buildFile .\psakefile.ps1 `
-taskList "${{ parameters.taskList }}" `
-parameters @{ Configuration = "${{ parameters.buildConfiguration }}" }
displayName: 'Run psake with parameters'
Template-Based Reusable Pipelines
Create templates/psake-build.yml:
# templates/psake-build.yml
parameters:
- name: taskList
type: string
default: 'Build, Test'
- name: configuration
type: string
default: 'Release'
steps:
- pwsh: |
Install-Module -Name psake -Scope CurrentUser -Force
displayName: 'Install psake'
- pwsh: |
Invoke-psake -buildFile .\psakefile.ps1 `
-taskList "${{ parameters.taskList }}" `
-parameters @{ Configuration = "${{ parameters.configuration }}" }
displayName: 'Run psake ${{ parameters.taskList }}'
Use the template:
# azure-pipelines.yml
trigger:
- main
pool:
vmImage: 'windows-latest'
jobs:
- job: BuildAndTest
steps:
- template: templates/psake-build.yml
parameters:
taskList: 'Build, Test'
configuration: 'Release'
Conditional Deployment Based on Branch
stages:
- stage: Build
jobs:
- job: BuildJob
steps:
- pwsh: Invoke-psake -taskList Build
displayName: 'Build'
- stage: DeployDev
condition: eq(variables['Build.SourceBranch'], 'refs/heads/develop')
jobs:
- job: DeployToDevJob
steps:
- pwsh: |
Invoke-psake -taskList Deploy -parameters @{ Environment = 'Development' }
- stage: DeployProd
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployToProdJob
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- pwsh: |
Invoke-psake -taskList Deploy -parameters @{ Environment = 'Production' }
See Also
- Installing psake - Installation guide
- Running psake - Basic usage
- Parameters and Properties - Parameterizing builds
- GitHub Actions - GitHub Actions integration
- .NET Solution Builds - .NET build examples