Azure DevOps YAML Pipeline Tutorial for Testers (With Examples)
A YAML pipeline tutorial written specifically for QA engineers. Learn YAML syntax, triggers, stages, jobs, steps, variables, conditions, and how to structure a complete testing pipeline — with annotated examples.
YAML pipelines are the modern way to define CI/CD in Azure DevOps. They're version-controlled, reviewable, and reproducible. This tutorial explains YAML pipeline syntax from a QA engineer's perspective — no infrastructure experience required.
YAML basics
YAML (YAML Ain't Markup Language) is a human-readable data format. Key rules:
# This is a comment
key: value # String value
number: 42 # Integer
boolean: true # Boolean
list: # List (array)
- item1
- item2
nested: # Nested object
child_key: child_value
multiline: | # Literal block (preserves newlines)
line one
line twoIndentation is critical — Azure DevOps YAML uses 2-space indentation. Tabs are not allowed.
Pipeline building blocks
Trigger
Controls when the pipeline runs:
# Run on push to main and release branches
trigger:
branches:
include:
- main
- release/*
exclude:
- release/old-*
# Run on pull requests to main
pr:
branches:
include:
- main
# Run on a schedule (1 AM UTC every weekday)
schedules:
- cron: "0 1 * * 1-5"
displayName: Nightly regression
branches:
include:
- main
always: truePool (where the pipeline runs)
pool:
vmImage: ubuntu-latest # Microsoft-hosted Linux
# vmImage: windows-latest # Microsoft-hosted Windows
# name: MyAgentPool # Self-hosted agent poolVariables
variables:
# Inline variable
NODE_VERSION: '20.x'
# Variable group from Library (for secrets)
- group: staging-env-vars
# Conditional variable
- name: runLongTests
value: ${{ eq(variables['Build.SourceBranch'], 'refs/heads/main') }}Stages
Stages group related jobs. They run sequentially by default:
stages:
- stage: Build
displayName: Build
jobs:
- job: BuildApp
steps: [...]
- stage: Test
displayName: Test
dependsOn: Build # Only runs if Build succeeds
jobs:
- job: RunTests
steps: [...]
- stage: Deploy
displayName: Deploy
dependsOn: Test
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployToStaging
environment: staging
steps: [...]Jobs
Jobs run on an agent. Multiple jobs in a stage run in parallel by default:
stages:
- stage: Test
jobs:
# These two jobs run in parallel
- job: UnitTests
steps:
- script: npm test:unit
- job: APITests
steps:
- script: npm test:api
# This job runs after both complete
- job: E2ETests
dependsOn:
- UnitTests
- APITests
condition: succeeded()
steps:
- script: npx playwright testSteps
Steps are the individual commands inside a job:
steps:
# Run a shell command
- script: npm ci
displayName: Install dependencies
# Run a PowerShell command (Windows)
- powershell: Write-Host "Hello from PowerShell"
displayName: PowerShell step
# Use a built-in task
- task: NodeTool@0
displayName: Setup Node.js
inputs:
versionSpec: '20.x'
# Use a task with condition
- task: PublishTestResults@2
displayName: Publish results
inputs:
testResultsFormat: JUnit
testResultsFiles: '**/results.xml'
condition: always() # Run even if previous steps failedConditions
Control when a step or job runs:
# Common conditions
condition: succeeded() # Only if previous step/stage passed (default)
condition: failed() # Only if previous step/stage failed
condition: always() # Regardless of previous result
condition: canceled() # Only if pipeline was cancelled
# Compound conditions
condition: and(succeeded(), eq(variables.runLongTests, 'true'))
condition: or(failed(), canceled())
# Branch-specific
condition: eq(variables['Build.SourceBranch'], 'refs/heads/main')Complete QA-focused YAML
trigger:
branches:
include: [main]
pr:
branches:
include: [main]
schedules:
- cron: "0 2 * * 1-5"
displayName: Nightly full regression
branches:
include: [main]
always: true
pool:
vmImage: ubuntu-latest
variables:
- group: qa-environment-secrets
- name: isCIRun
value: ${{ ne(variables['Build.Reason'], 'Schedule') }}
stages:
- stage: Smoke
displayName: Smoke Tests
jobs:
- job: Smoke
steps:
- task: NodeTool@0
inputs: { versionSpec: '20.x' }
- script: npm ci
- script: npx playwright test --grep @smoke
env:
BASE_URL: $(STAGING_URL)
- task: PublishTestResults@2
inputs:
testResultsFormat: JUnit
testResultsFiles: results/smoke.xml
testRunTitle: Smoke — $(Build.BuildNumber)
condition: always()
- stage: Regression
displayName: Full Regression
dependsOn: Smoke
condition: and(succeeded(), eq(variables.isCIRun, 'false'))
jobs:
- job: Playwright
timeoutInMinutes: 45
steps:
- task: NodeTool@0
inputs: { versionSpec: '20.x' }
- script: npm ci
- script: npx playwright install --with-deps
- script: npx playwright test --workers=4
env:
BASE_URL: $(STAGING_URL)
DB_URL: $(TEST_DB_URL)
- task: PublishTestResults@2
inputs:
testResultsFormat: JUnit
testResultsFiles: results/regression.xml
testRunTitle: Regression — $(Build.BuildNumber)
condition: always()
- task: PublishPipelineArtifact@1
inputs:
targetPath: playwright-report
artifact: regression-report
condition: always()Common errors and fixes
Error: "Unexpected value 'tab character'" YAML parsing error Fix: YAML requires spaces, not tabs. Use a YAML-aware editor (VS Code highlights tab characters in YAML files).
Error: Pipeline runs but stages skip without explanation
Fix: Check the condition on each stage. A stage with condition: succeeded() skips if any previous stage fails. Check upstream stage results.
Error: Variable from variable group shows as "$(VAR_NAME)" instead of the value Fix: The pipeline must be authorised to use the variable group. Go to Library → [Group] → Pipeline permissions → Authorize for use in all pipelines (or specifically authorize this pipeline).
Error: "The 'dependsOn' value 'StageName' is not valid"
Fix: dependsOn uses the stage name (the key under - stage:), not the displayName. Check the exact stage name value.
Stay ahead in AI-driven QA
Get practical tutorials on test automation, AI testing, and quality engineering — straight to your inbox. No spam, unsubscribe any time.
Discussion
Sign in with GitHub to comment · powered by Giscus