Azure DevOps: Microsoft's Platform for Repos, Pipelines, Boards, and Deployments
A practical guide to Azure DevOps — Microsoft's integrated platform spanning source control, CI/CD pipelines, work item tracking, package management, and test planning, covering each service, YAML pipeline authoring, release strategies, and how it compares to GitHub Actions.
Table of Contents
- Introduction
- The Five Services
- Azure Repos
- Azure Pipelines: YAML Fundamentals
- A Complete .NET CI/CD Pipeline
- Variables, Variable Groups, and Secrets
- Multi-Stage Pipelines and Environments
- Templates and Reusability
- Azure Boards
- Azure Artifacts
- Azure Test Plans
- Self-Hosted Agents
- Azure DevOps vs. GitHub Actions/GitHub
- Quick Reference Table
- Conclusion
Introduction
Azure DevOps is Microsoft's integrated suite for the full software delivery lifecycle — source control, build/release pipelines, work tracking, package feeds, and manual/exploratory test management, all under one platform with a shared permission model and a long history predating (and still commonly used alongside or instead of) GitHub Actions. Organizations that adopted it in its earlier incarnation as Team Foundation Server (TFS) often still run substantial parts of their delivery pipeline on it today, and it remains a common choice for enterprises wanting an integrated, Microsoft-native alternative that combines project management (Boards) with CI/CD in a single product.
trigger:
branches:
include: [main]
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UseDotNet@2
inputs:
version: '9.0.x'
- script: dotnet restore
- script: dotnet build --configuration Release
- script: dotnet test --configuration Release
That YAML pipeline definition — conceptually similar to a GitHub Actions workflow, but with Azure DevOps' own task-based syntax — is the modern, version-controlled way to define builds and releases in Azure DevOps, replacing the classic drag-and-drop pipeline editor that was the platform's original interface.
1. The Five Services
Azure DevOps is organized into five distinct but interconnected services, each independently usable:
| Service | Purpose |
|---|---|
| Azure Repos | Git (or legacy TFVC) source control repositories |
| Azure Pipelines | CI/CD build and release automation |
| Azure Boards | Work item tracking — backlogs, sprints, Kanban boards |
| Azure Artifacts | Package feeds (NuGet, npm, Maven, Python, universal packages) |
| Azure Test Plans | Manual and exploratory test case management |
A team can use all five together as a unified toolchain, or use only the pieces they need — a common pattern is using GitHub for source control while still using Azure Pipelines for CI/CD (and vice versa), since Azure Pipelines can build from a GitHub repository directly, and Azure Boards work items can link to GitHub commits and pull requests.
2. Azure Repos
Git repositories with familiar workflows
Azure Repos provides standard Git hosting — branches, pull requests, branch policies — functionally similar to GitHub or GitLab's core repository features, accessible via any Git client or IDE with Azure DevOps integration (Visual Studio has particularly deep native support).
Branch policies
Branch policies for `main`:
✓ Require a minimum number of reviewers (e.g., 2)
✓ Check for linked work items
✓ Require successful build validation before completing a PR
✓ Require comment resolution before merging
Branch policies enforce quality gates directly at the branch level — a pull request targeting a protected branch can be configured to require passing builds, a minimum reviewer count, and linked work items before it's allowed to merge, similar in spirit to GitHub's branch protection rules, with Azure DevOps' own particular set of configurable checks.
Pull requests
PR #142: Add discount calculation to checkout
Linked work item: #4521 (User Story: Support percentage discounts)
Reviewers: 2 required, 1 approved
Build validation: ✓ passed
Status: Ready to complete
Azure Repos pull requests support inline comments, required reviewers, automatic build validation triggers, and (distinctively) tight integration with Azure Boards work items — a PR can be directly linked to the user story or bug it addresses, and completing the PR can automatically transition that work item's state.
3. Azure Pipelines: YAML Fundamentals
Core structure
trigger:
branches:
include: [main, develop]
pr:
branches:
include: [main]
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
steps:
- task: UseDotNet@2
displayName: 'Install .NET SDK'
inputs:
version: '9.0.x'
- script: dotnet restore
displayName: 'Restore dependencies'
- script: dotnet build --configuration $(buildConfiguration)
displayName: 'Build'
- script: dotnet test --configuration $(buildConfiguration) --logger trx --results-directory $(Agent.TempDirectory)
displayName: 'Run tests'
- task: PublishTestResults@2
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '**/*.trx'
-
trigger— defines which branch pushes trigger a CI run (conceptually equivalent to GitHub Actions'on: push). -
pr— defines pull-request-triggered validation, separate from the CI trigger. -
pool— specifies which agent pool/VM image runs the pipeline (Microsoft-hosted or self-hosted, see Section 11). -
task— a reusable, versioned unit of pipeline logic (likeUseDotNet@2), analogous to a GitHub Action. -
script— a raw shell command, the simplest way to run something without a dedicated task.
Tasks vs. scripts
- task: DotNetCoreCLI@2
displayName: 'Build with the DotNetCoreCLI task'
inputs:
command: 'build'
arguments: '--configuration Release'
- script: dotnet build --configuration Release
displayName: 'Or just run it directly as a script'
Both approaches are common and valid — dedicated tasks (like DotNetCoreCLI@2) often provide richer input validation, cross-platform handling, and structured logging integration than an equivalent raw script, but a plain script step is simpler and more transparent for straightforward commands, mirroring the same "official action vs. raw shell command" choice available in GitHub Actions.
4. A Complete .NET CI/CD Pipeline
trigger:
branches:
include: [main]
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
stages:
- stage: Build
jobs:
- job: BuildAndTest
steps:
- task: UseDotNet@2
inputs:
version: '9.0.x'
- script: dotnet restore
displayName: 'Restore'
- script: dotnet build --configuration $(buildConfiguration) --no-restore
displayName: 'Build'
- script: dotnet test --configuration $(buildConfiguration) --no-build --logger trx
displayName: 'Test'
- task: PublishTestResults@2
condition: always()
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '**/*.trx'
- script: dotnet publish -c $(buildConfiguration) -o $(Build.ArtifactStagingDirectory)
displayName: 'Publish'
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'drop'
- stage: DeployStaging
dependsOn: Build
condition: succeeded()
jobs:
- deployment: DeployToStaging
environment: 'staging'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: drop
- task: AzureWebApp@1
inputs:
azureSubscription: 'my-service-connection'
appName: 'my-api-staging'
package: '$(Pipeline.Workspace)/drop/**/*.zip'
This mirrors the same build-then-deploy shape covered in this series' GitHub Actions guide: a Build stage that compiles, tests, and publishes an artifact, followed by a DeployStaging stage that only runs if Build succeeds, deploying the published artifact to an Azure Web App via a deployment job targeting a named environment (Section 6).
5. Variables, Variable Groups, and Secrets
Pipeline variables
variables:
buildConfiguration: 'Release'
dotnetVersion: '9.0.x'
- script: echo "Building with $(buildConfiguration) configuration"
Variables defined directly in the YAML are the simplest form — referenced with $(variableName) syntax throughout the pipeline.
Variable groups: shared, centrally-managed configuration
variables:
- group: 'production-config'
A variable group (managed under Pipelines → Library) centralizes variables shared across multiple pipelines — a connection string, a service URL, feature flag defaults — so they're maintained in one place rather than duplicated across every pipeline file that needs them, and can be scoped with their own permissions independent of the pipeline YAML itself.
Secret variables
variables:
- group: 'production-secrets' # variables marked as "secret" in the Library UI are automatically masked in logs
Secrets are marked as such in the Azure DevOps UI (not stored in plain text in the YAML file itself) — their values are automatically redacted from pipeline logs, similar to GitHub Actions' secret masking behavior, and can be sourced from Azure DevOps' own encrypted storage or linked directly to an Azure Key Vault for centralized secret management across both the pipeline and the applications it deploys.
Linking directly to Azure Key Vault
- task: AzureKeyVault@2
inputs:
azureSubscription: 'my-service-connection'
KeyVaultName: 'my-keyvault'
SecretsFilter: '*'
This task pulls secrets directly from an Azure Key Vault at pipeline run-time, making them available as pipeline variables for that run — avoiding duplicating secret values between Key Vault (where the application likely also sources them) and Azure DevOps' own variable storage.
6. Multi-Stage Pipelines and Environments
Stages: sequencing build and multiple deployment targets
stages:
- stage: Build
jobs: [ /* ... */ ]
- stage: DeployStaging
dependsOn: Build
jobs: [ /* ... */ ]
- stage: DeployProduction
dependsOn: DeployStaging
condition: succeeded()
jobs: [ /* ... */ ]
Stages are Azure Pipelines' top-level sequencing construct — conceptually similar to a chain of jobs across separate GitHub Actions workflows, but expressed within a single pipeline file, with dependsOn controlling execution order and condition controlling whether a stage proceeds.
Environments and approval gates
- stage: DeployProduction
jobs:
- deployment: DeployToProduction
environment: 'production' # references an Environment configured with its own approval checks
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
inputs:
appName: 'my-api-prod'
An Azure DevOps Environment (configured under Pipelines → Environments) can require manual approval from designated approvers, restrict which branches may deploy to it, and enforce business-hours deployment windows — directly analogous to GitHub's environment protection rules covered in this series' GitHub Actions guide, giving a genuine human gate on production deployments without a separate external approval tool.
Deployment strategies
strategy:
runOnce:
deploy:
steps: [ /* ... */ ]
# or, for a more gradual rollout:
strategy:
canary:
increments: [10, 20, 70]
deploy:
steps: [ /* ... */ ]
Azure Pipelines' deployment jobs support built-in strategies beyond a simple runOnce deploy — canary deployments (ramping traffic to the new version in increments) and rolling deployments are available as first-class YAML constructs, rather than needing to hand-roll the incremental rollout logic yourself.
7. Templates and Reusability
Step templates
# templates/build-steps.yml
parameters:
- name: dotnetVersion
type: string
default: '9.0.x'
steps:
- task: UseDotNet@2
inputs:
version: ${{ parameters.dotnetVersion }}
- script: dotnet restore
- script: dotnet build --configuration Release
# azure-pipelines.yml
steps:
- template: templates/build-steps.yml
parameters:
dotnetVersion: '9.0.x'
Templates let you extract a reusable sequence of steps (or entire jobs/stages) into a separate file, parameterized and referenced from multiple pipeline definitions — the direct equivalent of GitHub Actions' composite actions and reusable workflows, serving the same goal of avoiding copy-pasted, slowly-diverging pipeline logic across many repositories.
Extending a base template
# templates/base-pipeline.yml
parameters:
- name: environment
type: string
jobs:
- deployment: Deploy
environment: ${{ parameters.environment }}
strategy:
runOnce:
deploy:
steps:
- script: echo "Deploying to ${{ parameters.environment }}"
# azure-pipelines.yml
extends:
template: templates/base-pipeline.yml
parameters:
environment: 'production'
extends goes a step further than a plain template reference, letting an organization define a mandatory base pipeline structure (often used to enforce required security scanning steps or compliance checks across every team's pipeline) that individual repositories customize only within the parameters the base template explicitly allows — a governance mechanism GitHub Actions' reusable workflows don't have a direct equivalent for.
8. Azure Boards
Work item types and hierarchy
Epic: Q3 Checkout Redesign
└── Feature: Support percentage-based discounts
└── User Story: As a customer, I can apply a discount code at checkout
└── Task: Implement discount calculation logic
└── Task: Add discount code input field to checkout UI
└── Bug: Discount not applied when quantity > 10
Azure Boards organizes work into a configurable hierarchy — typically Epics, Features, User Stories/Product Backlog Items, Tasks, and Bugs — supporting both Scrum and Kanban-style process templates (Basic, Agile, Scrum, CMMI), selectable per project based on the team's preferred methodology.
Backlogs and sprints
Sprint 24 (Jul 14 - Jul 28)
Committed: 32 story points
├── #4521 - Support percentage discounts (5 pts) - In Progress
├── #4530 - Fix checkout validation bug (2 pts) - Done
└── #4535 - Add discount analytics dashboard (8 pts) - To Do
Sprints (iterations) organize work into time-boxed periods with capacity planning, burndown charts, and velocity tracking — the core Scrum ceremony support that's historically been one of Azure Boards' strongest differentiators against a pure CI/CD-focused tool.
Linking work items to code
Commit message: "Fix discount calculation #4521"
Referencing a work item ID in a commit message, branch name, or pull request automatically creates a traceable link — from a work item, you can see every commit, branch, build, and pull request associated with it, giving genuine end-to-end traceability from a business requirement through to the exact code and deployment that fulfilled it, a workflow that requires more manual stitching-together (via GitHub Issues + separate linking conventions) in a pure GitHub-based setup.
Kanban boards
To Do | In Progress | Code Review | Testing | Done
#4535 | #4521 | #4530 | | #4519
A visual, drag-and-drop board view of the current sprint or backlog, with configurable columns matching a team's actual workflow stages — functionally similar to a GitHub Projects board, with deeper native integration into the rest of Azure Boards' work item hierarchy and reporting.
9. Azure Artifacts
# Publishing a NuGet package to an Azure Artifacts feed
dotnet nuget push MyLibrary.1.0.0.nupkg --source "https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json" --api-key az
<!-- Consuming from the feed in another project -->
<PackageSource key="myfeed" value="https://pkgs.dev.azure.com/myorg/_packaging/myfeed/nuget/v3/index.json" />
Azure Artifacts hosts private package feeds — NuGet, npm, Maven, Python (pip), and a generic "universal packages" format — letting an organization publish and consume internal packages (a shared internal library, say) without needing a separate package registry product. Feeds support upstream sources, letting a private feed also proxy and cache packages from the public NuGet.org/npmjs.com registries, giving a single feed URL that resolves both internal and public packages, with the added benefit of caching public packages against upstream outages or a removed/yanked public package version.
10. Azure Test Plans
Test Suite: Checkout Flow
Test Case #201: Apply valid discount code
Steps:
1. Add item to cart → Expected: Item appears in cart
2. Enter valid discount code → Expected: Discount applied, total updated
3. Complete checkout → Expected: Order confirmed with discounted total
Status: Passed (last run: 2026-07-15)
Test Case #202: Apply expired discount code
Status: Failed — bug #4530 filed
Azure Test Plans manages structured manual and exploratory test cases — distinct from automated tests (which live in your actual codebase and run via Azure Pipelines) — giving QA teams a structured way to define test steps, track pass/fail status per test run, and automatically file a linked bug when a manual test fails. This is a genuinely distinctive piece of the Azure DevOps suite; GitHub's ecosystem has no direct first-party equivalent, relying instead on third-party test management tools or informal tracking via Issues for manual QA processes.
Linking automated test results
- task: PublishTestResults@2
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '**/*.trx'
Automated test results published from a pipeline (as shown in Section 4) surface in Azure DevOps' test reporting alongside manual Test Plans results, giving one combined view of both automated and manual test coverage and outcomes for a given build.
11. Self-Hosted Agents
Why self-host
pool:
name: 'MySelfHostedPool'
Microsoft-hosted agents (the default vmImage-based pool) are convenient and require no maintenance, but come with constraints: limited build minutes on free tiers, no persistent state between runs (every job starts from a completely fresh VM), and no access to resources behind a private network without additional configuration. Self-hosted agents — installed on your own VMs, on-prem servers, or containers — remove these constraints, at the cost of you now owning the agent's maintenance, patching, and scaling.
Setting up a self-hosted agent
./config.sh --url https://dev.azure.com/myorg --auth pat --token $PAT --pool MySelfHostedPool --agent myAgent1
./run.sh
A self-hosted agent registers itself with a specific agent pool, and pipelines that specify that pool's name will be scheduled onto it — common reasons to self-host include needing network access to an internal, non-internet-facing resource (an on-prem database, an internal API), needing specialized hardware (GPU-equipped build machines), or wanting persistent caching between runs without re-downloading dependencies every time (similar to the caching motivations covered in this series' GitHub Actions guide, but achieved here through the agent's own persistent filesystem rather than an explicit cache action).
12. Azure DevOps vs. GitHub Actions/GitHub
| Aspect | Azure DevOps | GitHub Actions + GitHub |
|---|---|---|
| Source control | Azure Repos (Git or legacy TFVC) | GitHub repositories |
| CI/CD | Azure Pipelines (YAML or classic UI) | GitHub Actions (YAML only) |
| Work tracking | Azure Boards — deep Scrum/Kanban support, work item hierarchy | GitHub Issues + Projects — lighter-weight, less formal process support |
| Package feeds | Azure Artifacts (NuGet, npm, Maven, Python, universal) | GitHub Packages (similar format support) |
| Manual test management | Azure Test Plans — dedicated, structured tool | No first-party equivalent; typically a third-party tool |
| Marketplace/ecosystem | Smaller marketplace of tasks | Much larger community Marketplace of Actions |
Governance (extends templates) |
Strong — mandatory base templates enforceable org-wide | Reusable workflows exist, but less of a hard "mandatory extension" mechanism |
| Typical organization fit | Enterprises wanting integrated project management + CI/CD in one product, often with existing Microsoft/Azure DevOps investment | Teams prioritizing open-source ecosystem size, GitHub-native workflows, broader community tooling |
They aren't mutually exclusive
A common, entirely supported setup: source code on GitHub, with Azure Pipelines still handling CI/CD (Azure Pipelines can build directly from a GitHub repository via a service connection), especially in organizations mid-migration between the two ecosystems, or specifically wanting Azure Pipelines' deployment strategy features or self-hosted agent flexibility while keeping GitHub's broader community/ecosystem benefits for the code itself.
Practical guidance
- Already invested in Azure DevOps, need deep Scrum/Kanban work tracking integrated with CI/CD, or need Azure Test Plans' manual test management? → Azure DevOps remains a strong, cohesive choice.
- Prioritizing the largest possible community/Marketplace ecosystem, already living in GitHub for source control, or building open-source software? → GitHub Actions is generally the more natural fit.
- Neither choice locks you out of the other — many organizations mix and match based on which specific service (Repos vs. Pipelines vs. Boards) best fits a given team's actual need.
Quick Reference Table
| Concept | Purpose |
|---|---|
| Azure Repos | Git-based source control with branch policies |
| Azure Pipelines | YAML-based CI/CD, stages/jobs/steps, deployment jobs |
| Variable groups | Centrally-managed, shared pipeline configuration |
| Environments | Approval gates and deployment history per target |
Templates / extends
|
Reusable and (optionally) organization-mandated pipeline structure |
| Azure Boards | Work item hierarchy, sprints, Kanban boards |
| Azure Artifacts | Private NuGet/npm/Maven/Python package feeds with upstream proxying |
| Azure Test Plans | Structured manual/exploratory test case management |
| Self-hosted agents | Custom build infrastructure for network access, hardware, or persistent caching needs |
Conclusion
Azure DevOps' distinguishing strength is breadth under one roof — source control, CI/CD, structured work tracking, package feeds, and manual test management, all sharing a permission model and cross-linking naturally (a commit referencing a work item, a pipeline run surfacing both automated and manual test results together). For organizations that want that level of integrated project management alongside their build and release automation — particularly ones already invested in the Microsoft ecosystem — it remains a genuinely strong, mature choice, with YAML pipelines today offering the same version-controlled, reviewable pipeline-as-code approach that GitHub Actions popularized.
The right choice between Azure DevOps and a GitHub-centric toolchain usually comes down to which surrounding ecosystem and process model fits a team better, not a meaningful gap in core CI/CD capability — both platforms can build, test, and deploy a .NET application reliably, and plenty of real organizations use pieces of both rather than treating it as an exclusive either/or decision.
Found this useful? Feel free to star the repo, open an issue with corrections, or share the pipeline template that saved your team from a dozen copy-pasted YAML files.
Top comments (0)