Azure DevOps YAML Pipeline for Testers
A YAML pipeline tutorial written specifically for QA engineers. Learn YAML syntax, triggers, stages, jobs, steps, variables, conditions, and how to.
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:
YAML1# This is a comment 2key: value # String value 3number: 42 # Integer 4boolean: true # Boolean 5list: # List (array) 6 - item1 7 - item2 8nested: # Nested object 9 child_key: child_value 10multiline: | # Literal block (preserves newlines) 11 line one 12 line two
Indentation is critical — Azure DevOps YAML uses 2-space indentation. Tabs are not allowed.
Pipeline building blocks
Trigger
Controls when the pipeline runs:
YAML1# Run on push to main and release branches 2trigger: 3 branches: 4 include: 5 - main 6 - release/* 7 exclude: 8 - release/old-* 9 10# Run on pull requests to main 11pr: 12 branches: 13 include: 14 - main 15 16# Run on a schedule (1 AM UTC every weekday) 17schedules: 18 - cron: "0 1 * * 1-5" 19 displayName: Nightly regression 20 branches: 21 include: 22 - main 23 always: true
Pool (where the pipeline runs)
YAML1pool: 2 vmImage: ubuntu-latest # Microsoft-hosted Linux 3 # vmImage: windows-latest # Microsoft-hosted Windows 4 # name: MyAgentPool # Self-hosted agent pool
Variables
YAML1variables: 2 # Inline variable 3 NODE_VERSION: '20.x' 4 5 # Variable group from Library (for secrets) 6 - group: staging-env-vars 7 8 # Conditional variable 9 - name: runLongTests 10 value: ${{ eq(variables['Build.SourceBranch'], 'refs/heads/main') }}
Stages
Stages group related jobs. They run sequentially by default:
YAML1stages: 2 - stage: Build 3 displayName: Build 4 jobs: 5 - job: BuildApp 6 steps: [...] 7 8 - stage: Test 9 displayName: Test 10 dependsOn: Build # Only runs if Build succeeds 11 jobs: 12 - job: RunTests 13 steps: [...] 14 15 - stage: Deploy 16 displayName: Deploy 17 dependsOn: Test 18 condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) 19 jobs: 20 - deployment: DeployToStaging 21 environment: staging 22 steps: [...]
Jobs
Jobs run on an agent. Multiple jobs in a stage run in parallel by default:
YAML1stages: 2 - stage: Test 3 jobs: 4 # These two jobs run in parallel 5 - job: UnitTests 6 steps: 7 - script: npm test:unit 8 9 - job: APITests 10 steps: 11 - script: npm test:api 12 13 # This job runs after both complete 14 - job: E2ETests 15 dependsOn: 16 - UnitTests 17 - APITests 18 condition: succeeded() 19 steps: 20 - script: npx playwright test
Steps
Steps are the individual commands inside a job:
YAML1steps: 2 # Run a shell command 3 - script: npm ci 4 displayName: Install dependencies 5 6 # Run a PowerShell command (Windows) 7 - powershell: Write-Host "Hello from PowerShell" 8 displayName: PowerShell step 9 10 # Use a built-in task 11 - task: NodeTool@0 12 displayName: Setup Node.js 13 inputs: 14 versionSpec: '20.x' 15 16 # Use a task with condition 17 - task: PublishTestResults@2 18 displayName: Publish results 19 inputs: 20 testResultsFormat: JUnit 21 testResultsFiles: '**/results.xml' 22 condition: always() # Run even if previous steps failed
Conditions
Control when a step or job runs:
YAML1# Common conditions 2condition: succeeded() # Only if previous step/stage passed (default) 3condition: failed() # Only if previous step/stage failed 4condition: always() # Regardless of previous result 5condition: canceled() # Only if pipeline was cancelled 6 7# Compound conditions 8condition: and(succeeded(), eq(variables.runLongTests, 'true')) 9condition: or(failed(), canceled()) 10 11# Branch-specific 12condition: eq(variables['Build.SourceBranch'], 'refs/heads/main')
Complete QA-focused YAML
YAML1trigger: 2 branches: 3 include: [main] 4 5pr: 6 branches: 7 include: [main] 8 9schedules: 10 - cron: "0 2 * * 1-5" 11 displayName: Nightly full regression 12 branches: 13 include: [main] 14 always: true 15 16pool: 17 vmImage: ubuntu-latest 18 19variables: 20 - group: qa-environment-secrets 21 - name: isCIRun 22 value: ${{ ne(variables['Build.Reason'], 'Schedule') }} 23 24stages: 25 - stage: Smoke 26 displayName: Smoke Tests 27 jobs: 28 - job: Smoke 29 steps: 30 - task: NodeTool@0 31 inputs: { versionSpec: '20.x' } 32 - script: npm ci 33 - script: npx playwright test --grep @smoke 34 env: 35 BASE_URL: $(STAGING_URL) 36 - task: PublishTestResults@2 37 inputs: 38 testResultsFormat: JUnit 39 testResultsFiles: results/smoke.xml 40 testRunTitle: Smoke — $(Build.BuildNumber) 41 condition: always() 42 43 - stage: Regression 44 displayName: Full Regression 45 dependsOn: Smoke 46 condition: and(succeeded(), eq(variables.isCIRun, 'false')) 47 jobs: 48 - job: Playwright 49 timeoutInMinutes: 45 50 steps: 51 - task: NodeTool@0 52 inputs: { versionSpec: '20.x' } 53 - script: npm ci 54 - script: npx playwright install --with-deps 55 - script: npx playwright test --workers=4 56 env: 57 BASE_URL: $(STAGING_URL) 58 DB_URL: $(TEST_DB_URL) 59 - task: PublishTestResults@2 60 inputs: 61 testResultsFormat: JUnit 62 testResultsFiles: results/regression.xml 63 testRunTitle: Regression — $(Build.BuildNumber) 64 condition: always() 65 - task: PublishPipelineArtifact@1 66 inputs: 67 targetPath: playwright-report 68 artifact: regression-report 69 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.
Share this article
Follow for more
Follow me on social media for more developer tips, tricks, and tutorials. Let's connect and build something great together!