DevOps 5 min read

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.

I
InnovateBits
InnovateBits

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:

YAML
1# 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:

YAML
1# 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)

YAML
1pool: 2 vmImage: ubuntu-latest # Microsoft-hosted Linux 3 # vmImage: windows-latest # Microsoft-hosted Windows 4 # name: MyAgentPool # Self-hosted agent pool

Variables

YAML
1variables: 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:

YAML
1stages: 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:

YAML
1stages: 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:

YAML
1steps: 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:

YAML
1# 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

YAML
1trigger: 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.

Tags
#azure-devops#yaml-pipeline#azure-pipelines#yaml-tutorial#test-automation#qa-engineers

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!