Skip to main content
Back to blog

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.

InnovateBits5 min read
Share

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 two

Indentation 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: true

Pool (where the pipeline runs)

pool:
  vmImage: ubuntu-latest      # Microsoft-hosted Linux
  # vmImage: windows-latest  # Microsoft-hosted Windows
  # name: MyAgentPool         # Self-hosted agent pool

Variables

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 test

Steps

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 failed

Conditions

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.

Free newsletter

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