Azure Pipelines: YAML from Scratch
Master Azure Pipelines YAML syntax β stages, jobs, steps, triggers, and pipeline structure. Build CI pipelines that compile, test, and validate your code automatically on every push.
What is Azure Pipelines YAML?
Think of a recipe card in a restaurant kitchen.
A recipe says: βFirst, prep the vegetables (stage 1). Then, cook the sauce (stage 2). Finally, plate the dish (stage 3).β Each stage has specific steps β chop onions, heat oil, add garlic. The recipe lives in the recipe book, and every chef follows the same instructions to produce a consistent dish.
Azure Pipelines YAML is the recipe for building and deploying software. It lives in your Git repository (the recipe book), and every pipeline run follows the same instructions. Stages are the major phases (build, test, deploy). Jobs are the workstreams within a stage. Steps are the individual commands β βinstall dependencies,β βrun tests,β βpublish artefacts.β
The YAML hierarchy
Every Azure Pipeline follows this structure:
Pipeline
βββ Stages
βββ Jobs
βββ Steps (scripts or tasks)
| Level | What It Represents | Runs On | Parallelism |
|---|---|---|---|
| Pipeline | The entire CI/CD workflow | N/A β container for stages | N/A |
| Stage | A major phase (Build, Test, Deploy) | A logical group of jobs | Stages run sequentially by default, parallel with dependsOn: [] |
| Job | A unit of work assigned to one agent | One agent (Microsoft-hosted or self-hosted) | Jobs within a stage run in parallel by default |
| Step | A single command or task | On the jobβs agent | Steps always run sequentially within a job |
Minimal pipeline β single stage, single job
When you have a simple CI build, you can skip the stages and jobs keywords:
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- script: echo Hello, World!
displayName: 'Run a simple script'
- script: dotnet build
displayName: 'Build the project'
Azure Pipelines automatically wraps this in a single stage (β__defaultβ) and a single job (βJobβ). This is fine for simple builds, but production pipelines almost always use explicit stages and jobs.
Full multi-stage pipeline
trigger:
branches:
include:
- main
- release/*
stages:
- stage: Build
displayName: 'Build and Test'
jobs:
- job: BuildJob
pool:
vmImage: 'ubuntu-latest'
steps:
- task: DotNetCoreCLI@2
inputs:
command: 'build'
- task: DotNetCoreCLI@2
inputs:
command: 'test'
arguments: '--collect:"XPlat Code Coverage"'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
ArtifactName: 'drop'
- stage: DeployStaging
displayName: 'Deploy to Staging'
dependsOn: Build
jobs:
- deployment: DeployToStaging
environment: 'staging'
strategy:
runOnce:
deploy:
steps:
- script: echo Deploying to staging...
Key observations:
dependsOn: Buildmakes the Deploy stage wait for Build to succeed- The
deploymentjob type enables environment tracking and approval gates - The
environmentkeyword links the job to an Azure DevOps environment for governance
Trigger types
Triggers define when a pipeline runs. The exam tests all five types:
CI trigger (continuous integration)
Runs when code is pushed to specified branches:
trigger:
branches:
include:
- main
- release/*
exclude:
- feature/experimental
paths:
include:
- src/**
exclude:
- docs/**
Path filters are powerful β a pipeline that only builds when src/ changes avoids unnecessary runs when you only edit documentation.
PR trigger (pull request validation)
Runs when a pull request targets specified branches:
pr:
branches:
include:
- main
paths:
include:
- src/**
Important distinction: CI triggers fire on push. PR triggers fire when a PR is opened, updated, or re-opened.
Scheduled trigger
Runs on a cron schedule:
schedules:
- cron: '0 2 * * 1-5'
displayName: 'Weeknight build at 2 AM'
branches:
include:
- main
always: true
The always: true flag means the pipeline runs even if no code has changed since the last run.
Pipeline trigger (pipeline completion)
Runs when another pipeline completes:
resources:
pipelines:
- pipeline: buildPipeline
source: 'MyApp-CI'
trigger:
branches:
include:
- main
This is how you chain a CI pipeline to a CD pipeline β the deployment pipeline triggers when the build pipeline succeeds.
Manual trigger
Any pipeline can be run manually from the Azure DevOps UI or REST API. You can also disable automatic triggers entirely:
trigger: none
pr: none
This creates a pipeline that only runs when manually queued or triggered by another pipeline.
Exam tip: trigger none vs trigger omitted
This is a common exam trap:
trigger: noneexplicitly disables CI triggers β the pipeline never runs on push- Omitting the trigger keyword entirely uses the default β which triggers on ALL branches
If you forget to add trigger: to your YAML, the pipeline runs on every push to every branch. Always be explicit.
Same applies to pr: β omitting it enables PR triggers on all branches (in Azure Repos). For GitHub repos, PR triggers are controlled by branch policies, not the YAML pr: keyword.
Connecting GitHub repos to Azure Pipelines
Azure Pipelines can build code from GitHub repositories. The connection requires:
- Service connection β a GitHub service connection in Azure DevOps (OAuth or PAT-based)
- Azure Pipelines GitHub App β installed on the GitHub organisation for deeper integration (check run annotations, PR comments)
- YAML file in the repo β
azure-pipelines.ymlat the root of the GitHub repository
Once connected, Azure Pipelines receives webhook events from GitHub (push, PR) and triggers pipeline runs. Build status is reported back to GitHub as check runs.
Why choose Azure Pipelines for a GitHub repo? When the team needs Azure Pipelines-specific features β YAML environments with approval gates, multi-stage release pipelines, Azure Boards integration for work item linking, or when the organisation already has Azure DevOps for project management.
Steps: scripts vs tasks
Steps are the atomic units of work in a pipeline:
Script steps
- script: npm install && npm test
displayName: 'Install and test'
- Runs a command on the agentβs shell (bash on Linux/macOS, cmd on Windows)
- Use
bash:for cross-platform bash scripts - Use
powershell:orpwsh:for PowerShell
Task steps
- task: DotNetCoreCLI@2
inputs:
command: 'build'
projects: '**/*.csproj'
- Tasks are pre-built, versioned, reusable automation units
- The
@2suffix pins the major version β you get minor/patch updates but not breaking changes - Available from the built-in task library or the Azure DevOps Marketplace
When to use which: Use tasks when one exists for your scenario (they handle edge cases, logging, and error handling). Use scripts for simple or custom commands where no task exists.
YAML vs Classic pipelines
| Feature | YAML Pipelines | Classic Pipelines |
|---|---|---|
| Definition | Code in a YAML file stored in the repository | UI-based β configured through the Azure DevOps web interface |
| Version control | Fully versioned with Git β every change is tracked, reviewable, and auditable | Not version-controlled β changes are tracked in revision history but not in Git |
| Branch-specific | Different branches can have different pipeline definitions | One pipeline definition applies to all branches |
| Templates | Full template support β reusable stages, jobs, steps across pipelines | Task groups provide limited reuse |
| Multi-stage | Native β stages, environments, deployment jobs, approvals | Build and release are separate systems (build definitions + release definitions) |
| PR review | Pipeline changes go through PR review like any code change | No PR review β changes take effect immediately |
| New features | All new pipeline features are YAML-first | Maintenance mode β no major new features |
| Migration | Recommended for all new pipelines | Migrate existing Classic pipelines to YAML when feasible |
Scenario: Nadia's migration from Classic to YAML at Meridian Insurance
π’ Nadia Petrov at Meridian Insurance inherited 47 Classic build definitions and 23 Classic release definitions. Dmitri (VP Engineering) wants everything in YAML for auditability β Elena (compliance) insists that pipeline changes go through the same approval process as code changes.
Nadiaβs migration plan:
- Export each Classic pipeline using the βView YAMLβ feature (Azure DevOps can generate YAML from Classic definitions)
- Refactor β donβt just export blindly. Combine build + release into a single multi-stage YAML pipeline
- Templates β extract common patterns (restore, build, test, publish) into shared YAML templates
- Branch policies β PR review required on
azure-pipelines.ymlchanges, so Elena gets her audit trail - Migrate incrementally β one pipeline per sprint, starting with the lowest-risk projects
Rashid (platform lead) handles the agent pool configuration. The YAML pipelines reference the same agent pools as Classic β no infrastructure changes needed.
Knowledge check
Nadia writes a new Azure Pipelines YAML file but forgets to include a 'trigger:' section. What happens when she pushes code to a feature branch?
Kai wants his Azure Pipeline to run only when files in the 'src/' directory change on the main branch. Documentation changes in 'docs/' should NOT trigger a build. Which trigger configuration achieves this?
Meridian Insurance has a CI pipeline (Build) and a separate CD pipeline (Deploy). Nadia wants the Deploy pipeline to run automatically whenever Build succeeds on the main branch. How should she configure this in YAML?
π¬ Video coming soon
Next up: GitHub Actions: Workflows from Scratch β master GitHubβs CI/CD platform with events, jobs, matrix strategies, and reusable workflows.