| name | ado-pipeline-optimization |
| description | Use when improving Azure Pipelines performance — caching dependencies, parallel job strategies, artifact management between stages, test result publishing, code coverage gates, and reducing pipeline runtime. |
ADO Pipeline Optimization
When to Use
- Pipeline taking more than 15 minutes and needs to be faster
- Wasting parallel job minutes on redundant work
- Test results not visible in Azure DevOps Tests tab
- Code coverage not being tracked or trending over time
- Artifact sharing between stages causing issues
- Flaky or redundant jobs increasing cost without adding value
Core Jobs
1. Dependency Caching
- task: Cache@2
inputs:
key: 'npm | "$(Agent.OS)" | package-lock.json'
restoreKeys: 'npm | "$(Agent.OS)"'
path: $(npm_config_cache)
displayName: Cache npm packages
- script: npm ci
- task: Cache@2
inputs:
key: 'nuget | "$(Agent.OS)" | **/packages.lock.json,!**/bin/**,!**/obj/**'
restoreKeys: 'nuget | "$(Agent.OS)"'
path: $(NUGET_PACKAGES)
displayName: Cache NuGet packages
- task: DotNetCoreCLI@2
inputs:
command: restore
feedsToUse: select
- task: Cache@2
inputs:
key: 'pip | "$(Agent.OS)" | requirements.txt'
restoreKeys: 'pip | "$(Agent.OS)"'
path: $(PIP_CACHE_DIR)
displayName: Cache pip packages
- script: pip install -r requirements.txt
Cache hits reduce dependency install time from 2-5 minutes down to seconds. The key must include the lockfile to invalidate when dependencies change.
2. Parallel Jobs Strategy
stages:
- stage: Validate
jobs:
- job: Lint
pool:
vmImage: ubuntu-latest
steps:
- script: npm run lint
- job: TypeCheck
pool:
vmImage: ubuntu-latest
steps:
- script: npm run typecheck
- job: UnitTests
pool:
vmImage: ubuntu-latest
steps:
- script: npm test
- job: BuildDocker
pool:
vmImage: ubuntu-latest
steps:
- script: docker build .
- stage: IntegrationTests
dependsOn: Validate
jobs:
- job: IntTests
steps:
- script: npm run test:integration
3. Test Splitting for Parallel Execution
jobs:
- job: TestSplit
strategy:
parallel: 4
pool:
vmImage: ubuntu-latest
steps:
- script: |
# Azure DevOps provides these variables automatically
# System.JobPositionInPhase: 1, 2, 3, or 4
# System.TotalJobsInPhase: 4
python -m pytest tests/ \
--splits $(System.TotalJobsInPhase) \
--group $(System.JobPositionInPhase) \
--junitxml=test-results-$(System.JobPositionInPhase).xml
- task: PublishTestResults@2
inputs:
testResultsFiles: test-results-$(System.JobPositionInPhase).xml
condition: always()
4. Publishing Test Results & Coverage
- task: DotNetCoreCLI@2
inputs:
command: test
arguments: '--collect:"XPlat Code Coverage" --results-directory $(Agent.TempDirectory)'
- task: PublishTestResults@2
inputs:
testResultsFormat: VSTest
testResultsFiles: '$(Agent.TempDirectory)/**/*.trx'
condition: always()
- task: PublishCodeCoverageResults@2
inputs:
summaryFileLocation: $(Agent.TempDirectory)/**/coverage.cobertura.xml
pathToSources: $(Build.SourcesDirectory)/src
condition: always()
- script: |
COVERAGE=$(python -c "import xml.etree.ElementTree as ET; \
tree=ET.parse('coverage.cobertura.xml'); \
root=tree.getroot(); \
print(round(float(root.attrib['line-rate'])*100))")
echo "Coverage: $COVERAGE%"
[ "$COVERAGE" -ge 80 ] || (echo "Coverage below 80%!" && exit 1)
5. Build Once, Deploy Many
- stage: Build
jobs:
- job: BuildJob
steps:
- script: dotnet publish -c Release -o $(Build.ArtifactStagingDirectory)/app
- task: PublishPipelineArtifact@1
inputs:
artifactName: app-package
targetPath: $(Build.ArtifactStagingDirectory)/app
- stage: Deploy_Dev
dependsOn: Build
jobs:
- deployment: DeployDev
environment: dev
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: app-package
- task: AzureWebApp@1
inputs:
appName: myapp-dev
package: $(Pipeline.Workspace)/app-package
- stage: Deploy_Prod
dependsOn: Deploy_Dev
jobs:
- deployment: DeployProd
environment: production
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: app-package
- task: AzureWebApp@1
inputs:
appName: myapp-prod
package: $(Pipeline.Workspace)/app-package
6. Pipeline Timing Analysis
az pipelines runs list \
--pipeline-ids 5 \
--project MyProject \
--result succeeded \
--top 10 \
--output table
az pipelines runs show \
--id 12345 \
--project MyProject \
--open
Key Concepts
- Cache task — stores and restores directories between pipeline runs; cache key must include lockfile hash to invalidate properly
- Parallel jobs — independent jobs in the same stage run simultaneously on separate agents; each consumes one parallel job slot
- Pipeline artifact — build outputs stored in Azure DevOps storage; fast download in subsequent stages without re-downloading from external sources
- Test splitting — distribute test files across multiple agents;
System.JobPositionInPhase and System.TotalJobsInPhase identify each shard
- Publish test results — sends test XML to Azure DevOps for display in Tests tab; must use
condition: always() so failures are visible
- restoreKeys — fallback cache keys tried in order when the exact key misses; enables partial cache hits
Checklist
Key Outputs
- Pipeline runtime reduced (target: less than 10 minutes for build and unit test stage)
- Test results visible in Azure DevOps Tests tab with pass/fail history
- Code coverage trending visible in Coverage tab over time
- Single build artifact reused across all deployment environments (identical binary)
Output Format
- 🔴 Critical — rebuilding application in every deploy stage (slow builds, inconsistent binary), test results not published (failures invisible in UI),
npm install instead of npm ci (non-deterministic)
- 🟡 Warning — no caching configured (reinstalling all dependencies on every run), all validation jobs sequential when they can run in parallel, cache key not including lockfile (cache never invalidates)
- 🟢 Suggestion — split large test suites across 4+ parallel agents, add code coverage threshold gate (fail if drops below 80%), review pipeline timeline to find the critical path bottleneck
Anti-Patterns
npm install instead of npm ci (ignores lockfile, produces non-deterministic installs)
- Missing
condition: always() on PublishTestResults (coverage data missing when tests fail — least useful time)
- Downloading full source code in deploy stage and recompiling (defeats the purpose of build artifacts)
- Cache key not including lockfile — cache never invalidates when
package.json changes
- Publishing artifacts to both drop location AND pipeline artifact (redundant, slows pipeline)
- Setting
maxParallel too high — can exceed available parallel job slots and cause queuing
Integration
ado-pipeline-design — the pipeline structure to optimize; parallelism requires well-separated stages
ado-pipeline-security — do not cache credential files or secrets alongside dependencies