Complete azure-pipelines toolkit with generation and validation capabilities
72
90%
Does it follow best practices?
Impact
—
No eval scenarios have been run
Passed
No known issues
This document outlines best practices for creating maintainable, secure, and efficient Azure Pipelines.
❌ Bad:
variables:
API_KEY: 'sk-1234567890abcdef'
PASSWORD: 'MyP@ssw0rd'✅ Good:
variables:
- group: 'my-secrets' # From variable group
- name: API_KEY
value: $(SecretApiKey) # From pipeline variables marked as secret❌ Bad:
pool:
vmImage: 'ubuntu-latest'
- task: Docker@2✅ Good:
pool:
vmImage: 'ubuntu-22.04' # Specific version
- task: Docker@2 # Specific major versionStore credentials in service connections, not in pipeline variables.
- task: Docker@2
inputs:
containerRegistry: 'myDockerRegistryServiceConnection' # Service connection
command: 'login'variables:
- name: API_TOKEN
value: $(SecretToken)
# In Azure DevOps UI, mark variable as secretUse the principle of least privilege for service connections and agent pools.
Cache dependencies to speed up builds.
- task: Cache@2
displayName: 'Cache npm packages'
inputs:
key: 'npm | "$(Agent.OS)" | package-lock.json'
restoreKeys: |
npm | "$(Agent.OS)"
path: $(Pipeline.Workspace)/.npm
- script: npm ci --cache $(Pipeline.Workspace)/.npm
displayName: 'Install dependencies'dependsOn and conditionUse explicit dependencies to run jobs in parallel when possible.
stages:
- stage: Build
jobs:
- job: BuildFrontend
steps:
- script: npm run build:frontend
- job: BuildBackend
steps:
- script: npm run build:backend
- stage: Test
dependsOn: Build
jobs:
- job: TestFrontend
dependsOn: [] # Can start immediately after Build stage
steps:
- script: npm test:frontend
- job: TestBackend
dependsOn: [] # Can start immediately after Build stage
steps:
- script: npm test:backendReduce clone time by limiting git history.
steps:
- checkout: self
clean: true
fetchDepth: 1 # Shallow cloneOnly publish what's needed and set expiration.
- task: PublishPipelineArtifact@1
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)/dist' # Only dist folder
artifact: 'webapp'
publishLocation: 'pipeline'
# Set retention in Azure DevOps project settingsTest across multiple configurations in parallel.
strategy:
matrix:
node18:
nodeVersion: '18'
node20:
nodeVersion: '20'
node22:
nodeVersion: '22'
maxParallel: 3 # Run 3 at a time- stage: Build
displayName: 'Build Application'
jobs:
- job: BuildJob
displayName: 'Build and Compile'
steps:
- script: npm run build
displayName: 'Build with npm'Separate concerns into stages for complex pipelines.
stages:
- stage: Build
displayName: 'Build Stage'
jobs: [...]
- stage: Test
displayName: 'Test Stage'
dependsOn: Build
jobs: [...]
- stage: Deploy
displayName: 'Deploy Stage'
dependsOn: Test
jobs: [...]Extract common logic into templates.
# templates/npm-build.yml
steps:
- task: NodeTool@0
inputs:
versionSpec: $(nodeVersion)
- task: Cache@2
inputs:
key: 'npm | "$(Agent.OS)" | package-lock.json'
path: $(Pipeline.Workspace)/.npm
- script: npm ci --cache $(Pipeline.Workspace)/.npm
- script: npm run build
# azure-pipelines.yml
steps:
- template: templates/npm-build.yml
parameters:
nodeVersion: '20'Organize variables in variable groups for different environments.
variables:
- group: 'dev-variables'
- group: 'common-variables'Add comments to explain complex logic.
# This pipeline builds the frontend and backend separately,
# then runs integration tests before deploying to staging.
stages:
- stage: Build
# We build frontend and backend in parallel to save time
jobs:
- job: BuildFrontend
# Frontend uses React and requires Node 20
steps: [...]# Stage names: PascalCase
- stage: BuildAndTest
# Job names: PascalCase
- job: BuildApplication
# Step displayNames: Sentence case
- script: echo "test"
displayName: 'Run integration tests'
# Variables: camelCase or snake_case (be consistent)
variables:
buildConfiguration: 'Release'
node_version: '20'stages:
- stage: Build
jobs: [...]
- stage: UnitTest
dependsOn: Build
jobs: [...]
- stage: IntegrationTest
dependsOn: Build
jobs: [...]
- stage: DeployStaging
dependsOn:
- UnitTest
- IntegrationTest
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
jobs: [...]
- stage: DeployProduction
dependsOn:
- UnitTest
- IntegrationTest
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs: [...]trigger:
branches:
include:
- main
- develop
- release/*
paths:
exclude:
- docs/**
- README.md
pr:
branches:
include:
- main
paths:
exclude:
- docs/**- deployment: DeployWeb
displayName: 'Deploy to Production'
pool:
vmImage: 'ubuntu-22.04'
environment:
name: production
resourceName: webapp
strategy:
runOnce:
deploy:
steps:
- script: echo "Deploying"Configure approvals in the environment settings in Azure DevOps.
# For zero-downtime deployments
strategy:
canary:
increments: [10, 25, 50, 100]
preDeploy:
steps:
- script: echo "Pre-deploy checks"
deploy:
steps:
- script: echo "Deploy to $(strategy.canary.increment)% of instances"
postDeploy:
steps:
- script: echo "Monitor deployment"strategy:
runOnce:
deploy:
steps:
- script: ./deploy.sh
on:
failure:
steps:
- script: ./rollback.sh
displayName: 'Rollback on failure'- script: npm test -- --coverage --ci --reporters=default --reporters=jest-junit
displayName: 'Run tests'
- task: PublishTestResults@2
condition: succeededOrFailed() # Always publish results
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '**/junit.xml'
failTaskOnFailedTests: true- task: PublishCodeCoverageResults@1
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage/cobertura-coverage.xml'- script: npm run lint
displayName: 'Run ESLint'
- script: npm audit
displayName: 'Security audit'
continueOnError: true # Don't fail build on audit issuesjobs:
- job: Build
timeoutInMinutes: 30 # Prevent hung jobs
cancelTimeoutInMinutes: 5# Always run cleanup
- script: ./cleanup.sh
displayName: 'Cleanup'
condition: always()
# Only on failure
- script: ./send-alert.sh
displayName: 'Send failure notification'
condition: failed()
# Only on success
- script: ./deploy.sh
displayName: 'Deploy'
condition: succeeded()- script: npm run lint
displayName: 'Run linter'
continueOnError: true # Don't fail the pipeline if linting failsparameters:
- name: deployToStaging
type: boolean
default: true
- name: deployToProduction
type: boolean
default: false
stages:
- stage: Build
jobs: [...]
- stage: DeployStaging
condition: eq(parameters.deployToStaging, true)
jobs:
- deployment: DeployStaging
environment: staging
- stage: DeployProduction
condition: and(succeeded(), eq(parameters.deployToProduction, true))
dependsOn:
- Build
- DeployStaging
jobs:
- deployment: DeployProduction
environment: productiontrigger:
branches:
include:
- main
- feature/*
# Only deploy from main
- stage: Deploy
condition: eq(variables['Build.SourceBranch'], 'refs/heads/main')
jobs: [...]pr:
branches:
include:
- main
paths:
include:
- src/**
stages:
- stage: PRValidation
jobs:
- job: BuildAndTest
steps:
- script: npm install
- script: npm run build
- script: npm test
- script: npm run lintlatest tags for images or tasks@0 for task versions (deprecated)Before committing your pipeline: