Complete azure-pipelines toolkit with generation and validation capabilities
97
97%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Advisory
Suggest reviewing before use
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: