From azure
Best practices and conventions for writing Azure DevOps Pipelines in YAML. Covers CI/CD pipeline structure, stages, jobs, steps, triggers, variable and parameter management, template reuse, build optimization, IaC validation in build stages, testing integration (unit, integration, E2E with JUnit/VSTest), deployment strategies (blue-green, canary, rolling), environment promotion, cross-stage variable passing with stageDependencies and ##vso[task.setvariable], multi-subscription deployment patterns, Bicep deployment with AzureResourceManagerTemplateDeployment, module publishing to ACR, security hardening (Key Vault, managed identities, approval gates), and performance tuning (caching, parallel jobs, matrix strategies). Apply this skill whenever creating, reviewing, modifying, or troubleshooting azure-pipelines.yml files, multi-stage YAML pipelines, pipeline templates, or any Azure DevOps pipeline configuration. Also apply when the user asks about Azure DevOps CI/CD conventions, pipeline YAML best practices, deployment strategies in Azure Pipelines, pipeline security, cross-stage outputs, or variable/template management -- even if they do not mention "Azure DevOps" by name.
npx claudepluginhub atc-net/atc-agentic-toolkit --plugin azureThis skill uses the workspace's default tool permissions.
Follow these conventions when creating or modifying Azure DevOps Pipelines YAML to produce pipelines that are secure, maintainable, and performant.
Generates Azure DevOps pipeline YAML for CI/CD, including multi-stage workflows, Docker builds, Kubernetes deploys, language-specific setups, and reusable templates.
Provides Azure DevOps YAML pipeline best practices on multi-stage structures, triggers, scheduling, variables, caching, templates, and security for efficient CI/CD.
Designing composable Azure DevOps YAML pipelines. Templates, variable groups, multi-stage, triggers.
Share bugs, ideas, or general feedback.
Follow these conventions when creating or modifying Azure DevOps Pipelines YAML to produce pipelines that are secure, maintainable, and performant.
Note: This skill focuses on YAML pipeline authoring conventions and best practices. For Azure Pipelines service reference documentation, see the
azure-pipelinesskill. For Azure DevOps CLI commands, see theazure-devops-cliskill.
name or displayName so the run UI is easy to scancondition, continueOnError, and status-check functions (succeeded(), failed(), always())dependsOn between stages and jobs to express ordering and fan-in/fan-out patternscondition at every level for conditional execution based on branch, variable, or prior outcomestages, jobs, steps, variables) and reference them with template:resources: repositoriesFor detailed pipeline structure guidance and examples, see references/pipeline-structure.md.
latest -- this prevents surprise breakage when images updateCache@2 task) to reduce restore timesPublishBuildArtifacts@1 or PublishPipelineArtifact@1 and set retention policies$(Build.BuildId), semantic version via a variable or script)az bicep build --file main.bicep) to catch syntax and type errors earlyPublishTestResults@2 with testResultsFormat set appropriatelyPublishCodeCoverageResults@2failTaskOnFailedTests: true to fail the pipeline on test failures rather than silently continuingFor detailed testing patterns and examples, see references/testing.md.
AzureKeyVault@2 task -- never hardcode secrets in YAMLFor detailed security guidance and Key Vault examples, see references/security.md.
deployment:) with environment: targeting for deployment tracking and approvalsrunOnce, rolling, canary via the strategy: blockFor detailed deployment patterns and rollback examples, see references/deployment.md.
parameters:) to accept input at queue time for flexibilitycondition with variable values for conditional logicisSecret: true in variable groups or use Key Vault referencesvariables.yml template file for environment-specific values (service connections, subscription IDs, environment names) and import it with template: variables.ymlFor detailed variable and parameter patterns, see references/variables-and-parameters.md.
Passing outputs between stages and jobs is essential for IaC pipelines where deployment outputs (resource names, connection strings) feed into subsequent stages.
Use ##vso[task.setvariable] in PowerShell or Bash steps to export dynamic values as pipeline variables:
- task: PowerShell@2
name: bicep_outputs
displayName: Export deployment outputs
inputs:
targetType: inline
script: |
($env:DEPLOYMENT_OUTPUT | ConvertFrom-Json).PSObject.Properties | ForEach-Object {
Write-Output "##vso[task.setvariable variable=$($_.Name);isOutput=true]$($_.Value.value)"
}
Mark variables with isOutput=true to make them accessible from other jobs and stages.
Use stageDependencies to reference outputs from prior stages. The syntax depends on whether the source is a regular job or a deployment job:
variables:
# From a deployment job in a prior stage
- name: resourceGroupName
value: $[ stageDependencies.DeployInfra.deploy.outputs['deploy.bicep_outputs.resourceGroupName'] ]
# From a regular job in a prior stage
- name: buildVersion
value: $[ stageDependencies.Build.build.outputs['version.buildVersion'] ]
For outputs between jobs within the same stage, use dependencies instead of stageDependencies:
variables:
- name: keyVaultName
value: $[ dependencies.setup.outputs['setup.step_name.keyVaultName'] ]
For enterprise environments spanning multiple Azure subscriptions, use per-environment service connections and pass subscription-specific parameters through templates:
# variables.yml
variables:
devServiceConnection: 'Platform - DEV - Service Connection'
prodServiceConnection: 'Platform - PROD - Service Connection'
# azure-pipelines.yml
stages:
- stage: deploy_dev
jobs:
- template: templates/environment.yml
parameters:
environment: DEV
serviceConnection: ${{ variables.devServiceConnection }}
subscriptionId: ${{ variables.devSubscriptionId }}
- stage: deploy_prod
dependsOn: deploy_dev
jobs:
- template: templates/environment.yml
parameters:
environment: PROD
serviceConnection: ${{ variables.prodServiceConnection }}
subscriptionId: ${{ variables.prodSubscriptionId }}
For deploying Bicep templates at subscription scope with parameter files:
- task: AzureResourceManagerTemplateDeployment@3
displayName: Deploy bicep template
inputs:
deploymentName: 'environment-${{ lower(parameters.environment) }}-$(Build.BuildNumber)'
deploymentScope: Subscription
deploymentOutputs: DEPLOYMENT_OUTPUT
azureResourceManagerConnection: ${{ parameters.serviceConnection }}
subscriptionId: ${{ parameters.subscriptionId }}
location: ${{ parameters.location }}
csmFile: $(Pipeline.Workspace)/drop/deploy/bicep/main.bicep
csmParametersFile: $(Pipeline.Workspace)/drop/deploy/bicep/main.${{ lower(parameters.environment) }}.bicepparam
overrideParameters: '-location "${{ parameters.location }}" -environment "${{ parameters.environment }}"'
Capture outputs with deploymentOutputs and export them with ##vso[task.setvariable] for downstream stages.
Automate publishing of versioned Bicep modules to Azure Container Registry in a dedicated pipeline stage:
- stage: publish_modules
displayName: Publish IaC bicep modules
jobs:
- deployment: deploy
strategy:
runOnce:
deploy:
steps:
- task: AzurePowerShell@5
displayName: Push bicep modules to container registry
inputs:
azureSubscription: ${{ parameters.serviceConnection }}
azurePowerShellVersion: latestVersion
scriptType: InlineScript
inline: |
Get-ChildItem -Path "$(modulePath)" -Filter "*.bicep" -Recurse | ForEach-Object {
$module = # extract module name and version from filename
Publish-AzBicepModule -FilePath $_.FullName -Target "br:$registry/bicep/modules/$module"
}
dependsOnCache@2fetchDepth: 1) when full git history is not neededFor detailed optimization patterns, see references/optimization.md.
trigger: for CI on push events; filter by branch and pathpr: for pull request validation; include appropriate branch and path filtersschedules: for maintenance tasks (nightly builds, dependency updates)resources: pipelines: triggers to chain pipelines (e.g., build completion triggers deployment)trigger: nonetrigger:
branches:
include:
- main
- release/*
paths:
exclude:
- docs/*
- '*.md'
pr:
branches:
include:
- main
parameters:
- name: deployEnvironment
displayName: 'Deploy to environment'
type: string
default: 'dev'
values:
- dev
- staging
- production
variables:
- group: common-settings
- name: buildConfiguration
value: 'Release'
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: BuildJob
displayName: 'Build Application'
pool:
vmImage: 'ubuntu-22.04'
steps:
- checkout: self
fetchDepth: 1
- task: Cache@2
displayName: 'Cache NuGet packages'
inputs:
key: 'nuget | "$(Agent.OS)" | **/packages.lock.json'
restoreKeys: |
nuget | "$(Agent.OS)"
path: $(Pipeline.Workspace)/.nuget/packages
- task: DotNetCoreCLI@2
displayName: 'Restore dependencies'
inputs:
command: restore
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: build
arguments: '--configuration $(buildConfiguration) --no-restore'
- task: DotNetCoreCLI@2
displayName: 'Run unit tests'
inputs:
command: test
arguments: '--configuration $(buildConfiguration) --no-build --collect:"XPlat Code Coverage"'
publishTestResults: true
- task: PublishCodeCoverageResults@2
displayName: 'Publish code coverage'
inputs:
summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'
- task: PublishPipelineArtifact@1
displayName: 'Publish build artifact'
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)'
artifactName: 'drop'
- stage: DeployDev
displayName: 'Deploy to Dev'
dependsOn: Build
condition: and(succeeded(), eq('${{ parameters.deployEnvironment }}', 'dev'))
jobs:
- deployment: DeployDev
displayName: 'Deploy to Dev Environment'
environment: 'dev'
strategy:
runOnce:
deploy:
steps:
- task: AzureKeyVault@2
displayName: 'Fetch secrets from Key Vault'
inputs:
azureSubscription: 'dev-service-connection'
KeyVaultName: 'kv-myapp-dev'
SecretsFilter: '*'
- task: AzureWebApp@1
displayName: 'Deploy to Azure Web App'
inputs:
azureSubscription: 'dev-service-connection'
appName: 'webapp-myapp-dev'
package: '$(Pipeline.Workspace)/drop/**/*.zip'
- stage: DeployProd
displayName: 'Deploy to Production'
dependsOn: DeployDev
condition: and(succeeded(), eq('${{ parameters.deployEnvironment }}', 'production'))
jobs:
- deployment: DeployProd
displayName: 'Deploy to Production'
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: AzureKeyVault@2
displayName: 'Fetch secrets from Key Vault'
inputs:
azureSubscription: 'prod-service-connection'
KeyVaultName: 'kv-myapp-prod'
SecretsFilter: '*'
- task: AzureWebApp@1
displayName: 'Deploy to Azure Web App'
inputs:
azureSubscription: 'prod-service-connection'
appName: 'webapp-myapp-prod'
package: '$(Pipeline.Workspace)/drop/**/*.zip'
| Anti-Pattern | Why It Is Harmful | Better Approach |
|---|---|---|
| Hardcoded secrets in YAML | Secrets visible in source control; security breach risk | Use variable groups linked to Azure Key Vault |
| Single monolithic pipeline file | Difficult to maintain, test, and reuse across projects | Break into templates for stages, jobs, and steps |
Using latest VM images | Builds break unpredictably when the image updates | Pin to a specific image version (e.g., ubuntu-22.04) |
No dependsOn or condition | Stages run unconditionally, wasting time and resources | Use dependsOn for ordering and condition for gating |
Ignoring test failures (continueOnError: true) | Broken code proceeds to deployment; bugs reach production | Set failTaskOnFailedTests: true; fail the build on test failures |
| Duplicated steps across pipelines | Maintenance burden; drift between pipeline copies | Extract into step or job templates; share via a templates repo |
| Overly broad service connection permissions | Violates least privilege; larger blast radius if compromised | Scope service connections to specific resource groups and roles |
| No caching of dependencies | Every build re-downloads packages; slow feedback loops | Use Cache@2 with hashFiles-based keys |
| Skipping approval gates for production | Accidental or unauthorized production deployments | Configure environment approvals and branch control checks |
| Deep git clones when not needed | Wastes time and bandwidth fetching full history | Use fetchDepth: 1 for shallow clones when history is irrelevant |
| Polling for pipeline chaining | Wasteful and slow; adds unnecessary delay | Use resources: pipelines: triggers for event-driven chaining |
| No timeout on jobs | Stuck jobs consume agent capacity indefinitely | Set timeoutInMinutes on every job |
Use this checklist when creating or reviewing Azure DevOps Pipeline YAML files.
name or the file is descriptively namedtrigger and pr settings with branch and path filtersdisplayNamedependsOn correctly expresses ordering between stages and jobscondition is used for conditional execution where appropriateCache@2PublishTestResults@2)failTaskOnFailedTests: true is setenvironment: for tracking and approvalsrunOnce, rolling, canary) is chosen appropriatelyfetchDepth: 1) is used where full history is not neededtimeoutInMinutes is set on all jobsresources: repositories