Level: Intermediate
Estimated duration: 4–5 hours
Last updated: June 2026
Table of Contents
- Azure Repos – Source Control
- YAML Pipelines – Structure and Syntax
- Pull Requests and Merge Strategies
- Tests in Pipelines
- Azure Artifacts
- Advanced Pipeline Scenarios
- Complete Code Examples
- Comparison Tables
- Glossary
1. Azure Repos – Source Control
Azure Repos Overview
Azure Repos is the source control management service integrated into Azure DevOps. It supports Git (recommended) and TFVC (Team Foundation Version Control, legacy). In a modern DevOps workflow, Azure Repos serves as the source of truth for all code and configurations.
graph TD
subgraph DEV["Developers"]
D1[Dev 1\nVS Code + Git]
D2[Dev 2\nVisual Studio]
D3[Dev 3\nJetBrains]
end
subgraph REPOS["Azure Repos"]
MAIN["main branch\n(protected)"]
FEATURE["feature/* branches\n(short-lived)"]
HOTFIX["hotfix/* branches\n(urgent fixes)"]
end
subgraph POLICIES["Branch Policies"]
REVIEW["Code Review\n(min 2 reviewers)"]
CI["Build Validation\n(CI must pass)"]
WORKITEM["Work Items\n(traceability)"]
COMMENTS["Comments Resolved\n(all resolved)"]
end
subgraph PIPELINE["Azure Pipelines"]
CI_PIPE["CI Pipeline\n(build + test)"]
CD_PIPE["CD Pipeline\n(deploy)"]
end
DEV -->|Push| FEATURE
FEATURE -->|Pull Request| MAIN
POLICIES -->|Gates| MAIN
MAIN -->|Trigger| CI_PIPE
CI_PIPE -->|Artifacts| CD_PIPE
style DEV fill:#1e3a5f,color:#fff
style REPOS fill:#7c3aed,color:#fff
style POLICIES fill:#f97316,color:#fff
style PIPELINE fill:#10b981,color:#fff
Configuration and Authentication
# ===== INITIAL GIT CONFIGURATION =====
# Configure Git identity
git config --global user.name "First Last"
git config --global user.email "user@company.com"
# Configure Azure Credential Manager (recommended)
# Windows: built into Git for Windows 2.x
# macOS: brew install git-credential-manager
# Linux: git credential-manager configure
# ===== CLONE A REPOSITORY =====
# Via HTTPS (Azure Credential Manager handles the token)
git clone https://dev.azure.com/{organisation}/{project}/_git/{repo}
# Via SSH (requires an SSH key configured in DevOps Settings)
git clone git@ssh.dev.azure.com:v3/{organisation}/{project}/{repo}
# ===== BASIC WORKFLOW =====
# Create a feature branch
git checkout -b feature/JIRA-123-add-user-auth main
# Develop, commit with conventional commits
git add .
git commit -m "feat: add JWT authentication middleware"
git commit -m "test: add unit tests for auth middleware"
# Push the branch
git push origin feature/JIRA-123-add-user-auth
# Create the PR (via portal or CLI)
az repos pr create \
--repository my-repo \
--source-branch feature/JIRA-123-add-user-auth \
--target-branch main \
--title "feat: add JWT authentication middleware" \
--description "Implements JWT authentication. Resolves JIRA-123." \
--reviewers "lead-dev@company.com" "appsec@company.com"
Branch Policies
Branch Policies protect important branches (main, develop, release/*) by enforcing controls before any merge.
flowchart TD
PUSH[Push on\nfeature branch]
PR[Create\nPull Request]
subgraph CHECKS["Branch Policy Checks"]
REVIEWERS["✅ Min 2 reviewers\n(4-eyes principle)"]
CI_BUILD["✅ CI build passes\n(Build Validation)"]
COMMENTS2["✅ Comments\nresolved"]
WORKITEMS["✅ Work Items\nassociated"]
CONFLICTS["✅ No\nmerge conflicts"]
end
APPROVED{All\npolicies pass?}
MERGE["Merge to main\n(authorized)"]
BLOCKED["Merge blocked\n(fix first)"]
PUSH --> PR
PR --> CHECKS
CHECKS --> APPROVED
APPROVED -->|Yes| MERGE
APPROVED -->|No| BLOCKED
BLOCKED -->|Fixes| PR
style CHECKS fill:#1e3a5f,color:#fff
style MERGE fill:#10b981,color:#fff
style BLOCKED fill:#ef4444,color:#fff
Configure Branch Policies via Azure CLI:
# Enable build validation on the main branch
REPO_ID=$(az repos show \
--repository my-repo \
--project my-project \
--query id \
--output tsv)
# Create a minimum reviewers policy (2)
az repos policy reviewer-count create \
--allow-downvotes false \
--blocking true \
--branch main \
--enabled true \
--minimum-approver-count 2 \
--repository-id $REPO_ID \
--project my-project
# Create a build validation policy
PIPELINE_ID=$(az pipelines show \
--name "CI-my-app" \
--project my-project \
--query id \
--output tsv)
az repos policy build create \
--blocking true \
--branch main \
--build-definition-id $PIPELINE_ID \
--display-name "CI Build Validation" \
--enabled true \
--manual-queue-only false \
--queue-on-source-update-only true \
--repository-id $REPO_ID \
--valid-duration 720 \
--project my-project
# Create a comment resolution policy
az repos policy comment-required create \
--blocking true \
--branch main \
--enabled true \
--repository-id $REPO_ID \
--project my-project
Security and Azure Repos Permissions
# ===== ENTRA ID CONNECTION =====
# Connect Azure DevOps to Microsoft Entra ID
# (Done in Organization Settings → Azure Active Directory)
# ===== REPO PERMISSIONS =====
# View groups and permissions
az devops security permission list \
--subject "group@company.com" \
--namespace-id "2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87" \
--output table
# ===== AUDIT LOGS =====
# See who did what in the repo
az devops audit stream list --project my-project
# ===== PERSONAL ACCESS TOKENS (PATs) =====
# PATs are used for:
# - Automation scripts
# - Self-hosted CI/CD agents
# - Third-party integrations
# Create a PAT via the interface:
# User Settings → Personal access tokens → New Token
# Recommended scopes (least privilege):
# - Code: Read & write (for agents)
# - Build: Read & execute (for pipelines)
2. YAML Pipelines – Structure and Syntax
YAML Pipeline Architecture
graph TD
subgraph PIPELINE["azure-pipelines.yml"]
TRIGGER["trigger:\nbranches, paths, tags"]
VARS_ROOT["variables:\n(scope: entire pipeline)"]
subgraph S1["Stage: Build"]
VARS_STAGE["variables:\n(scope: stage)"]
subgraph J1["Job: Compile"]
VARS_JOB["variables:\n(scope: job)"]
T1["Task: DotNetCoreCLI\nrestore"]
T2["Task: DotNetCoreCLI\nbuild"]
T3["Task: DotNetCoreCLI\ntest"]
T4["Task: PublishBuildArtifacts"]
end
end
subgraph S2["Stage: Deploy"]
subgraph J2["Deployment Job"]
ENV["environment: production\n(with approvals)"]
T5["Task: AzureWebApp\ndeploy"]
end
end
end
S1 -->|"dependsOn (implicit)"| S2
style PIPELINE fill:#1e3a5f,color:#fff
style S1 fill:#3b82f6,color:#fff
style S2 fill:#10b981,color:#fff
Complete YAML Pipeline: .NET Application
# azure-pipelines.yml
# Complete CI/CD pipeline for a .NET GloboTicket application
trigger:
branches:
include:
- main
- 'release/*'
- develop
paths:
include:
- 'src/**'
- 'tests/**'
exclude:
- 'docs/**'
- '*.md'
pr:
branches:
include:
- main
- develop
paths:
include:
- 'src/**'
- 'tests/**'
# ===== PIPELINE VARIABLES =====
variables:
buildConfiguration: 'Release'
dotNetVersion: '8.x'
solution: '**/*.sln'
testProjects: '**/*Tests.csproj'
# Conditional variables based on branch
${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}:
deployEnvironment: 'production'
azureRG: 'rg-prod'
${{ else }}:
deployEnvironment: 'staging'
azureRG: 'rg-staging'
stages:
# ============================================================
# STAGE 1: Build and Tests
# ============================================================
- stage: Build
displayName: '🔨 Build & Test'
jobs:
- job: BuildTest
displayName: 'Compile, Test & Analyze'
pool:
vmImage: 'ubuntu-latest'
steps:
# Setup .NET
- task: UseDotNet@2
displayName: 'Setup .NET $(dotNetVersion)'
inputs:
version: '$(dotNetVersion)'
packageType: 'sdk'
# Restore packages
- task: DotNetCoreCLI@2
displayName: 'Restore NuGet packages'
inputs:
command: 'restore'
projects: '$(solution)'
arguments: '--locked-mode' # Reproduce exact package versions
# Build
- task: DotNetCoreCLI@2
displayName: 'Build solution'
inputs:
command: 'build'
projects: '$(solution)'
arguments: '--configuration $(buildConfiguration) --no-restore'
# Unit tests with code coverage
- task: DotNetCoreCLI@2
displayName: 'Run unit tests'
inputs:
command: 'test'
projects: '$(testProjects)'
arguments: >-
--configuration $(buildConfiguration)
--no-build
--collect:"XPlat Code Coverage"
--results-directory $(Agent.TempDirectory)
--logger:"trx;LogFileName=test-results.trx"
-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura
# Publish test results
- task: PublishTestResults@2
displayName: 'Publish test results'
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '**/test-results.trx'
testRunTitle: 'Unit Tests - Build $(Build.BuildId)'
failTaskOnFailedTests: true
condition: always()
# Publish code coverage
- task: PublishCodeCoverageResults@2
displayName: 'Publish code coverage'
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'
reportDirectory: '$(Build.ArtifactStagingDirectory)/coverage'
condition: always()
# Publish build artifacts
- task: DotNetCoreCLI@2
displayName: 'Publish application'
inputs:
command: 'publish'
projects: '**/GloboTicket.Web.csproj'
arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)/app'
publishWebProjects: true
- task: PublishBuildArtifacts@1
displayName: 'Publish build artifacts'
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'drop'
publishLocation: 'Container'
# ============================================================
# STAGE 2: Docker Image Build
# ============================================================
- stage: Docker
displayName: '🐳 Docker Build & Push'
dependsOn: Build
condition: succeeded()
jobs:
- job: DockerBuildPush
displayName: 'Build & Push Docker image'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: AzureCLI@2
displayName: 'Login to ACR'
inputs:
azureSubscription: 'AzureServiceConnection'
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az acr login --name myacr
- task: Docker@2
displayName: 'Build Docker image'
inputs:
command: build
repository: 'myacr.azurecr.io/globoticket-web'
Dockerfile: 'src/GloboTicket.Web/Dockerfile'
tags: |
$(Build.BuildId)
${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}:
latest
- task: Docker@2
displayName: 'Push to ACR'
inputs:
command: push
repository: 'myacr.azurecr.io/globoticket-web'
tags: |
$(Build.BuildId)
# ============================================================
# STAGE 3: Deployment
# ============================================================
- stage: Deploy
displayName: '🚀 Deploy to $(deployEnvironment)'
dependsOn:
- Build
- Docker
condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest'))
jobs:
- deployment: DeployApp
displayName: 'Deploy to $(deployEnvironment)'
pool:
vmImage: 'ubuntu-latest'
environment: '$(deployEnvironment)'
strategy:
runOnce:
deploy:
steps:
- download: current
artifact: 'drop'
- task: AzureWebApp@1
displayName: 'Deploy to Azure App Service'
inputs:
azureSubscription: 'AzureServiceConnection'
appType: 'webApp'
appName: 'globoticket-web'
package: '$(Pipeline.Workspace)/drop/app/**/*.zip'
deploymentMethod: 'runFromPackage'
- script: |
# Post-deployment health check
APP_URL=$(az webapp show \
--resource-group $(azureRG) \
--name globoticket-web \
--query "defaultHostName" \
--output tsv)
echo "Testing https://$APP_URL/health"
sleep 30
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://$APP_URL/health")
if [ "$STATUS" -ne 200 ]; then
echo "❌ Health check failed! HTTP $STATUS"
exit 1
fi
echo "✅ Health check passed! HTTP $STATUS"
displayName: 'Post-deployment health check'
env:
AZURE_SUBSCRIPTION: 'AzureServiceConnection'
Build Agents: Microsoft-hosted vs Self-hosted
# ===== MICROSOFT-HOSTED AGENTS =====
# Ubuntu (Linux)
pool:
vmImage: 'ubuntu-latest' # Ubuntu 22.04
# Windows
pool:
vmImage: 'windows-latest' # Windows Server 2022
# macOS (for iOS/macOS builds)
pool:
vmImage: 'macOS-latest' # macOS 14
# ===== SELF-HOSTED AGENTS =====
# By pool name
pool:
name: 'SelfHosted-Linux'
# By pool name with demands (requirements)
pool:
name: 'SelfHosted-Linux'
demands:
- docker # Agent must have Docker
- node >= 20 # Node.js version 20 or higher
# Agentless (for approvals, gates)
pool: server
Install and register a self-hosted agent:
# ===== LINUX =====
# Create the agent directory
mkdir ~/azdo-agent && cd ~/azdo-agent
# Download the agent
AGENT_VERSION="3.248.0"
curl -Lo agent.tar.gz "https://vstsagentpackage.azureedge.net/agent/${AGENT_VERSION}/vsts-agent-linux-x64-${AGENT_VERSION}.tar.gz"
tar xzf agent.tar.gz
# Configure (use a PAT as authentication token)
./config.sh \
--unattended \
--url "https://dev.azure.com/my-organisation" \
--auth pat \
--token "YOUR_PAT_HERE" \
--pool "SelfHosted-Linux" \
--agent "$(hostname)" \
--acceptTeeEula
# Install as Linux service
sudo ./svc.sh install
sudo ./svc.sh start
sudo ./svc.sh status
# ===== WINDOWS (PowerShell) =====
$agentVersion = "3.248.0"
$agentUrl = "https://vstsagentpackage.azureedge.net/agent/$agentVersion/vsts-agent-win-x64-$agentVersion.zip"
New-Item -ItemType Directory -Path "C:\azdo-agent" -Force
Set-Location "C:\azdo-agent"
Invoke-WebRequest -Uri $agentUrl -OutFile agent.zip
Expand-Archive -Path agent.zip -DestinationPath .
.\config.cmd `
--unattended `
--url "https://dev.azure.com/my-organisation" `
--auth pat `
--token "YOUR_PAT_HERE" `
--pool "SelfHosted-Windows" `
--agent $env:COMPUTERNAME `
--acceptTeeEula
# Install as Windows service
.\svc.cmd install
.\svc.cmd start
.\svc.cmd status
Variables and Variable Groups
# ===== VARIABLE TYPES =====
# 1. Inline variables in YAML
variables:
buildConfig: 'Release'
imageTag: '$(Build.BuildId)'
# 2. Alternative syntax (more explicit)
variables:
- name: buildConfig
value: 'Release'
- name: imageTag
value: '$(Build.BuildId)'
# Read-only variable
- name: secretToken
value: 'never-put-here' # Use UI secrets instead
readonly: true
# 3. Variable Group
variables:
- group: 'production-secrets' # Defined in Library
# 4. Combination
variables:
- group: 'production-secrets'
- name: appName
value: 'my-app'
- name: resourceGroup
value: 'rg-prod'
# ===== VARIABLE SCOPES =====
variables:
pipelineVar: 'accessible everywhere'
stages:
- stage: Build
variables:
stageVar: 'accessible in Build only'
jobs:
- job: MyJob
variables:
jobVar: 'accessible in MyJob only'
steps:
- script: |
echo "Pipeline: $(pipelineVar)"
echo "Stage: $(stageVar)"
echo "Job: $(jobVar)"
# ===== PREDEFINED VARIABLES (System variables) =====
# $(Build.BuildId) - Unique build ID
# $(Build.BuildNumber) - Build number (e.g. 20240115.1)
# $(Build.SourceBranch) - Full branch name (refs/heads/main)
# $(Build.SourceBranchName) - Short name (main)
# $(Build.Repository.Name) - Repository name
# $(Build.ArtifactStagingDirectory) - Temporary folder for artifacts
# $(Agent.BuildDirectory) - Agent build directory
# $(Pipeline.Workspace) - Pipeline workspace
# ===== SECRETS VIA VARIABLE GROUPS LINKED TO KEY VAULT =====
# In Library → Variable Group:
# → "Link secrets from Azure Key Vault"
# → Select subscription and Key Vault
# → Choose secrets: DB_PASSWORD, API_KEY, etc.
# Use in pipeline:
variables:
- group: 'kv-production-secrets'
steps:
- script: echo "DB_PASSWORD=$(DB_PASSWORD)" # *** in logs
env:
DB_PASSWORD: $(DB_PASSWORD) # Inject secret as env variable
Complete Triggers
# ===== CI TRIGGER (push trigger) =====
trigger:
branches:
include:
- main
- 'release/v*'
- develop
exclude:
- 'experimental/*'
paths:
include:
- 'src/**'
- 'tests/**'
exclude:
- '**/*.md'
- 'docs/**'
tags:
include:
- 'v*' # Any tag starting with v
# Disable CI trigger
# trigger: none
# ===== PR TRIGGER =====
pr:
branches:
include:
- main
- develop
autoCancel: true # Cancel old PR builds if new push
# ===== SCHEDULED TRIGGER =====
schedules:
# Nightly build Monday through Friday
- cron: '0 2 * * 1-5'
displayName: 'Nightly build (Mon-Fri 2AM UTC)'
branches:
include:
- main
always: true # Even without code changes
# Weekly build on Sunday
- cron: '0 4 * * 0'
displayName: 'Weekly security scan (Sunday 4AM UTC)'
branches:
include:
- main
always: true
# ===== PIPELINE COMPLETION TRIGGER =====
resources:
pipelines:
- pipeline: infrastructure-pipeline # Local alias
source: 'MyProject/Infrastructure' # Source pipeline name
trigger:
branches:
include:
- main
stages:
- DeployStaging # Only if this stage succeeded
3. Pull Requests and Merge Strategies
Complete Pull Request Workflow
sequenceDiagram
participant DEV as Developer
participant BRANCH as Feature Branch
participant PR as Pull Request
participant REVIEW as Code Reviewers
participant CI as CI Pipeline
participant MAIN as main branch
DEV->>BRANCH: git checkout -b feature/JIRA-123
DEV->>BRANCH: git commit (conventional commits)
DEV->>PR: git push + create PR
Note over PR: Branch Policies evaluated
par CI Build
CI->>BRANCH: Build validation
CI-->>PR: ✅ CI passes
and Code Review
REVIEW->>PR: Code review
REVIEW->>PR: Comments
DEV->>PR: Reply/fix
REVIEW->>PR: ✅ Approve
end
PR->>PR: ✅ All policies satisfied
DEV->>MAIN: Squash merge
Note over MAIN: Clean history\n1 commit per PR
Git Merge Strategies
graph LR
subgraph BEFORE["Before merge"]
M1[main: A---B---C]
F1[feature: A---B---D---E]
end
subgraph SQUASH["Squash Merge"]
M2[main: A---B---C---F]
NOTE2["F = D+E\ncombined"]
end
subgraph REBASE["Rebase + Fast-forward"]
M3[main: A---B---C---D'---E']
NOTE3["D', E' = commits\nreplayed on C"]
end
subgraph NOFF["No Fast-forward (Merge commit)"]
M4[main: A---B---C---M]
NOTE4["M = merge commit\nD and E visible"]
end
BEFORE --> SQUASH
BEFORE --> REBASE
BEFORE --> NOFF
style SQUASH fill:#10b981,color:#fff
style REBASE fill:#3b82f6,color:#fff
style NOFF fill:#f59e0b,color:#000
| Strategy | History | Clarity | Complexity | Recommended |
|---|---|---|---|---|
| Squash | Very clean | ✅ One commit per PR | Simple | ✅ Agile teams |
| Rebase | Linear | ✅ Each commit visible | Moderate | ✅ For detailed history |
| Fast-forward | Linear (if possible) | ✅ Simple | Low | ✅ Small teams |
| No fast-forward | Merge commits | ⚠️ More noise | Simple | For explicit traceability |
Conventional Commits and Git Hygiene
# ===== CONVENTIONAL COMMITS =====
# Format: <type>(<scope>): <description>
# Types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert
# New features
git commit -m "feat(auth): add JWT authentication middleware"
git commit -m "feat(cart): implement checkout workflow"
# Bug fixes
git commit -m "fix(api): resolve null reference in UserService"
git commit -m "fix(cart): correct total calculation for discounted items"
# Documentation
git commit -m "docs(api): update OpenAPI specification"
# Tests
git commit -m "test(auth): add unit tests for token validation"
# Configuration/infrastructure
git commit -m "chore: upgrade .NET to 8.0"
git commit -m "ci: add Trivy container scan to pipeline"
# Refactoring
git commit -m "refactor(payment): extract payment processing to service"
# Performance
git commit -m "perf(db): add index on orders.customer_id"
# BREAKING CHANGE (in the commit body)
git commit -m "feat(api)!: change authentication endpoint to /v2/auth
BREAKING CHANGE: The authentication endpoint has moved from /api/auth to /v2/auth.
Update all clients to use the new endpoint."
# ===== GITFLOW VS TRUNK-BASED =====
# GitFlow: long-lived branches
# main (production) ← develop ← feature branches
# Release branches, hotfix branches
# Trunk-Based Development (recommended for DevOps):
# Everything to main, feature flags for incomplete features
# Short pull requests (< 1 day of code)
4. Tests in Pipelines
Layered Test Strategy
pie title Test Pyramid Azure DevOps
"E2E Tests - Playwright" : 5
"Integration Tests" : 20
"Unit Tests" : 75
graph LR
subgraph UNIT["Unit Tests\n(75% - fast)"]
U1["XUnit/.NET Tests\nxunit, NUnit, MSTest"]
U2["Jest/Mocha Tests\n(JavaScript)"]
U3["PyTest Tests\n(Python)"]
end
subgraph INT["Integration Tests\n(20% - moderate)"]
I1["API Tests\nTestcontainers"]
I2["DB Tests\nSQL Server docker"]
I3["Service Tests"]
end
subgraph E2E["E2E Tests\n(5% - slow)"]
E1["Browser Tests\nPlaywright"]
E2["Load Tests\nk6, Azure Load Test"]
E3["API Smoke Tests"]
end
CI_PIPELINE[CI Pipeline] --> UNIT
CI_PIPELINE --> INT
CD_PIPELINE[CD Pipeline] --> E2E
style UNIT fill:#10b981,color:#fff
style INT fill:#f59e0b,color:#000
style E2E fill:#ef4444,color:#fff
.NET Unit Tests in Pipelines
# .NET tests with code coverage
- stage: Test
jobs:
- job: UnitTests
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UseDotNet@2
inputs:
version: '8.x'
- task: DotNetCoreCLI@2
displayName: 'Restore'
inputs:
command: restore
projects: '**/*.sln'
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: build
projects: '**/*.sln'
arguments: '--configuration Release --no-restore'
- task: DotNetCoreCLI@2
displayName: 'Run Unit Tests'
inputs:
command: test
projects: '**/*UnitTests.csproj'
arguments: >-
--configuration Release
--no-build
--collect:"XPlat Code Coverage"
--results-directory $(Build.SourcesDirectory)/TestResults
--logger:"trx;LogFileName=unit-tests.trx"
-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura
# Code coverage threshold (fail if < 80%)
- task: DotNetCoreCLI@2
displayName: 'Check Code Coverage Threshold'
inputs:
command: custom
custom: tool
arguments: run reportgenerator -reports:"$(Build.SourcesDirectory)/TestResults/**/coverage.cobertura.xml" -targetdir:"$(Build.SourcesDirectory)/TestResults/CoverageReport" -reporttypes:"TextSummary;HtmlInline_AzurePipelines"
- script: |
COVERAGE=$(cat $(Build.SourcesDirectory)/TestResults/CoverageReport/Summary.txt | grep "Line coverage:" | awk '{print $3}' | tr -d '%')
echo "Code coverage: $COVERAGE%"
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "❌ Code coverage $COVERAGE% is below threshold of 80%!"
exit 1
fi
echo "✅ Code coverage $COVERAGE% meets threshold of 80%"
displayName: 'Validate Coverage Threshold'
- task: PublishTestResults@2
displayName: 'Publish Test Results'
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '**/unit-tests.trx'
testRunTitle: 'Unit Tests'
failTaskOnFailedTests: true
condition: always()
- task: PublishCodeCoverageResults@2
displayName: 'Publish Code Coverage'
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '**/coverage.cobertura.xml'
pathToSources: '$(Build.SourcesDirectory)/src'
condition: always()
E2E Tests with Playwright
# Playwright E2E tests in the CD pipeline
- stage: PlaywrightTests
displayName: 'E2E Tests (Playwright)'
dependsOn: DeployStaging
jobs:
- job: E2ETests
displayName: 'Playwright E2E Tests'
pool:
vmImage: 'ubuntu-latest'
steps:
# Download test artifacts (published in CI)
- download: current
artifact: playwright-tests
displayName: 'Download Playwright test artifacts'
# Extract the ZIP
- task: ExtractFiles@1
displayName: 'Extract Playwright tests'
inputs:
archiveFilePatterns: '$(Pipeline.Workspace)/playwright-tests/**/*.zip'
destinationFolder: '$(Agent.TempDirectory)/playwright'
# Install Playwright browsers
- script: |
cd $(Agent.TempDirectory)/playwright
npm ci
npx playwright install chromium firefox webkit
displayName: 'Install Playwright browsers'
# Run tests
- script: |
cd $(Agent.TempDirectory)/playwright
npx playwright test \
--config=playwright.config.ts \
--reporter=junit \
--reporter=html \
--output-folder=test-results
displayName: 'Run Playwright E2E Tests'
env:
BASE_URL: 'https://staging.my-app.com'
CI: 'true'
# Publish results
- task: PublishTestResults@2
displayName: 'Publish Playwright Results'
inputs:
testResultsFormat: 'JUnit'
testResultsFiles: '**/test-results.xml'
testRunTitle: 'Playwright E2E Tests'
failTaskOnFailedTests: true
condition: always()
# Publish HTML report (for failures)
- task: PublishBuildArtifacts@1
displayName: 'Publish Playwright HTML Report'
inputs:
PathtoPublish: '$(Agent.TempDirectory)/playwright/playwright-report'
ArtifactName: 'playwright-html-report'
condition: always()
5. Azure Artifacts
Azure Artifacts Architecture
Azure Artifacts is the package manager integrated into Azure DevOps. It allows creating private feeds (package registries) that serve as a secure proxy for all packages used by your teams.
graph TD
subgraph UPSTREAM["Upstream Sources (public)"]
NUGET[NuGet.org]
NPM_PUB[npmjs.com]
MAVEN[Maven Central]
PYPI[PyPI]
end
subgraph ARTIFACTS["Azure Artifacts Feed"]
CACHE["Package cache\n(public proxy)"]
INTERNAL["Internal packages\n(your libraries)"]
CURATED["Approved packages\n(whitelist)"]
end
subgraph CONSUMERS["Consumers"]
DEV1[Dev locally\n.npmrc, nuget.config]
PIPELINE[CI/CD Pipeline\nrestore packages]
DOCKER[Dockerfile\nnpm install]
end
UPSTREAM -->|Cache| ARTIFACTS
ARTIFACTS --> CONSUMERS
style UPSTREAM fill:#6b7280,color:#fff
style ARTIFACTS fill:#3b82f6,color:#fff
style CONSUMERS fill:#10b981,color:#fff
Create and Configure a Feed
# ===== CREATE A FEED =====
# Organizational feed (shared across all projects)
az artifacts feed create \
--name "company-feed" \
--public false \
--project my-project \
--organization https://dev.azure.com/my-org
# Project feed (limited to one project)
az artifacts feed create \
--name "project-internal-packages" \
--project my-project \
--scope project
# ===== CONFIGURE NuGet FOR THE FEED =====
# Create or modify nuget.config
cat > nuget.config << 'EOF'
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<clear />
<!-- Azure Artifacts feed as main proxy -->
<add key="AzureArtifacts"
value="https://pkgs.dev.azure.com/my-org/_packaging/company-feed/nuget/v3/index.json" />
</packageSources>
<packageSourceCredentials>
<AzureArtifacts>
<add key="Username" value="PAT" />
<!-- Token injected by the pipeline -->
<add key="ClearTextPassword" value="%AZURE_ARTIFACTS_PAT%" />
</AzureArtifacts>
</packageSourceCredentials>
</configuration>
EOF
# ===== CONFIGURE NPM FOR THE FEED =====
# Create .npmrc
cat > .npmrc << 'EOF'
registry=https://pkgs.dev.azure.com/my-org/_packaging/company-feed/npm/registry/
always-auth=true
EOF
# NPM authentication with PAT
echo "//pkgs.dev.azure.com/my-org/_packaging/company-feed/npm/registry/:_authToken=${AZURE_ARTIFACTS_PAT}" >> ~/.npmrc
Publish Internal Packages
# Publish an internal NuGet package
- stage: PublishPackage
displayName: 'Publish NuGet Package'
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- job: PublishNuGet
pool:
vmImage: 'ubuntu-latest'
steps:
- task: UseDotNet@2
inputs:
version: '8.x'
# Automatic versioning with GitVersion
- task: gitversion/setup@1
displayName: 'Install GitVersion'
inputs:
versionSpec: '5.x'
- task: gitversion/execute@1
displayName: 'Determine version'
name: gitversion
# Build the NuGet package
- task: DotNetCoreCLI@2
displayName: 'Build NuGet package'
inputs:
command: pack
projects: '**/MyLibrary.csproj'
arguments: >-
--configuration Release
-p:PackageVersion=$(gitversion.SemVer)
--output $(Build.ArtifactStagingDirectory)/packages
# Publish to Azure Artifacts
- task: DotNetCoreCLI@2
displayName: 'Push to Azure Artifacts'
inputs:
command: push
packagesToPush: '$(Build.ArtifactStagingDirectory)/packages/*.nupkg'
nuGetFeedType: 'internal'
publishVstsFeed: 'company-feed'
allowPackageConflicts: false # Fail if version already exists
6. Advanced Pipeline Scenarios
Reusable YAML Templates
# templates/build-dotnet.yml - Reusable template
parameters:
- name: solution
type: string
default: '**/*.sln'
- name: buildConfig
type: string
default: 'Release'
- name: dotnetVersion
type: string
default: '8.x'
- name: testProjects
type: string
default: '**/*Tests.csproj'
- name: publishProject
type: string
default: ''
- name: coverageThreshold
type: number
default: 80
steps:
- task: UseDotNet@2
displayName: 'Setup .NET ${{ parameters.dotnetVersion }}'
inputs:
version: '${{ parameters.dotnetVersion }}'
- task: DotNetCoreCLI@2
displayName: 'Restore packages'
inputs:
command: restore
projects: '${{ parameters.solution }}'
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: build
projects: '${{ parameters.solution }}'
arguments: '--configuration ${{ parameters.buildConfig }} --no-restore'
- task: DotNetCoreCLI@2
displayName: 'Test'
inputs:
command: test
projects: '${{ parameters.testProjects }}'
arguments: '--configuration ${{ parameters.buildConfig }} --no-build --collect:"XPlat Code Coverage"'
- ${{ if ne(parameters.publishProject, '') }}:
- task: DotNetCoreCLI@2
displayName: 'Publish'
inputs:
command: publish
projects: '${{ parameters.publishProject }}'
arguments: '--configuration ${{ parameters.buildConfig }} --output $(Build.ArtifactStagingDirectory)/app'
---
# azure-pipelines.yml - Use the template
trigger:
branches:
include:
- main
stages:
- stage: Build
jobs:
- job: BuildApp
pool:
vmImage: 'ubuntu-latest'
steps:
- template: templates/build-dotnet.yml
parameters:
solution: 'GloboTicket.sln'
buildConfig: 'Release'
testProjects: '**/*UnitTests.csproj'
publishProject: 'src/GloboTicket.Web/GloboTicket.Web.csproj'
coverageThreshold: 80
Parallel Matrix Strategy
# Tests on multiple configurations in parallel
jobs:
- job: IntegrationTests
displayName: 'Integration Tests'
strategy:
matrix:
SQLServer2019:
dbVersion: 'sql2019'
dbImage: 'mcr.microsoft.com/mssql/server:2019-latest'
SQLServer2022:
dbVersion: 'sql2022'
dbImage: 'mcr.microsoft.com/mssql/server:2022-latest'
PostgreSQL:
dbVersion: 'postgresql'
dbImage: 'postgres:16'
maxParallel: 3
pool:
vmImage: 'ubuntu-latest'
services:
database:
image: $(dbImage)
env:
ACCEPT_EULA: 'Y'
SA_PASSWORD: 'Test@12345'
steps:
- script: |
echo "Testing with $(dbVersion)"
dotnet test \
--configuration Release \
-- TestRunParameters.Parameter(name=\"DatabaseType\", value=\"$(dbVersion)\")
displayName: 'Run integration tests with $(dbVersion)'
7. Complete Code Examples
Full CD Pipeline with Playwright and AKS Deployment
# azure-pipelines-cd.yml - Full Continuous Delivery
trigger:
branches:
include:
- main
resources:
pipelines:
- pipeline: ci-pipeline
source: 'MyProject/CI-GloboTicket'
trigger:
branches:
include:
- main
variables:
- group: 'production-secrets'
- name: acrName
value: 'myacr'
- name: aksCluster
value: 'aks-prod'
- name: aksRG
value: 'rg-prod'
- name: namespace
value: 'production'
stages:
- stage: DeployStaging
displayName: 'Deploy to Staging'
jobs:
- deployment: DeployToStaging
environment: staging
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- task: AzureCLI@2
displayName: 'Deploy to AKS Staging'
inputs:
azureSubscription: 'AzureServiceConnection'
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
# Get AKS credentials
az aks get-credentials \
--resource-group rg-staging \
--name aks-staging \
--overwrite-existing
# Update the image in the deployment
kubectl set image deployment/globoticket \
globoticket=$(acrName).azurecr.io/globoticket:$(resources.pipeline.ci-pipeline.runID) \
-n staging
# Wait for deployment to be ready
kubectl rollout status deployment/globoticket -n staging
- stage: StagingTests
displayName: 'E2E Tests on Staging'
dependsOn: DeployStaging
jobs:
- job: PlaywrightTests
pool:
vmImage: 'ubuntu-latest'
steps:
- checkout: self
- script: |
npm ci
npx playwright install --with-deps chromium
npx playwright test --reporter=junit
displayName: 'Run Playwright tests'
env:
BASE_URL: 'https://staging.globoticket.com'
- task: PublishTestResults@2
inputs:
testResultsFiles: '**/test-results.xml'
testRunTitle: 'E2E Staging Tests'
condition: always()
- stage: DeployProduction
displayName: 'Deploy to Production'
dependsOn: StagingTests
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployToProduction
environment: production # Requires approval
pool:
vmImage: 'ubuntu-latest'
strategy:
runOnce:
deploy:
steps:
- task: AzureCLI@2
displayName: 'Deploy to AKS Production'
inputs:
azureSubscription: 'AzureServiceConnection'
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az aks get-credentials \
--resource-group $(aksRG) \
--name $(aksCluster) \
--overwrite-existing
# Rolling update
kubectl set image deployment/globoticket \
globoticket=$(acrName).azurecr.io/globoticket:$(resources.pipeline.ci-pipeline.runID) \
-n $(namespace)
# Verify the rollout
kubectl rollout status deployment/globoticket -n $(namespace)
# Health check
sleep 60
PROD_URL=$(kubectl get ingress globoticket -n $(namespace) -o jsonpath='{.spec.rules[0].host}')
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://$PROD_URL/health")
if [ "$HTTP_STATUS" -ne 200 ]; then
echo "❌ Health check failed after deployment! Initiating rollback..."
kubectl rollout undo deployment/globoticket -n $(namespace)
exit 1
fi
echo "✅ Production deployment successful!"
8. Comparison Tables
Merge Strategies
| Strategy | History | Git Complexity | Recommended For |
|---|---|---|---|
| Squash | Very clean (1 commit/PR) | Low | Most teams |
| Rebase | Linear with detail | Moderate | Advanced Git teams |
| Fast-forward | Linear if possible | Low | Small teams, small PRs |
| Merge commit | Non-linear | Low | Explicit traceability |
Test Types in Pipelines
| Type | Speed | Reliability | Execution Cost | Pipeline Stage |
|---|---|---|---|---|
| Unit tests | ⚡ < 1 min | ✅ Very reliable | $ Very low | CI - always |
| Integration tests | 🐌 1-5 min | ✅ Reliable | $$ Low | CI - on PR |
| E2E tests | 🐢 5-30 min | ⚠️ Fragile | $$$ Moderate | CD - staging only |
| Load tests | 🐢 30-60 min | ✅ Reliable | $$$$ High | Release - periodic |
Azure Artifacts vs GitHub Packages vs Public Registries
| Criteria | Azure Artifacts | GitHub Packages | NuGet.org / npmjs |
|---|---|---|---|
| ADO integration | ✅ Native | ✅ Via actions | ❌ External |
| Private packages | ✅ | ✅ | ❌ (paid) |
| Upstream proxy | ✅ Curated feeds | ❌ | N/A |
| Audit trail | ✅ | ✅ | ❌ |
| RBAC | ✅ Azure AD | ✅ GitHub perms | ❌ |
| Pricing | Included in ADO | Included GitHub | Free (public) |
9. Glossary
| Term | Definition |
|---|---|
| Azure Repos | Git source control management service integrated into Azure DevOps |
| Branch Policy | Protection rules applied to Azure Repos branches |
| Build Validation | Policy requiring CI to succeed before a PR merge |
| 4-eyes principle | Requires at least 2 approvals on a PR (compliance) |
| YAML Pipeline | CI/CD pipeline defined as YAML code in the repository |
| Stage | Logical grouping of Jobs in a YAML pipeline |
| Job | Set of Steps running on a single agent |
| Step | Unit of work in a Job (Task or Script) |
| Task | Reusable automation plugin (e.g. DotNetCoreCLI@2) |
| Agent Pool | Set of build agents available for pipelines |
| Microsoft-hosted Agent | Agent managed by Microsoft, reset after each run |
| Self-hosted Agent | Agent installed on a VM you manage yourself |
| Variable Group | Group of variables shared across multiple pipelines |
| Pipeline Library | Azure DevOps section for storing Variable Groups and Secure Files |
| Service Connection | Secure connection to an external service (Azure, GitHub, etc.) |
| Trigger | Condition triggering pipeline execution |
| PR Trigger | Triggered when a Pull Request is created or updated |
| Scheduled Trigger | Triggered according to a cron expression |
| Squash Merge | Strategy combining all PR commits into one |
| Conventional Commits | Standard for structured commit messages (feat, fix, docs, etc.) |
| Azure Artifacts | Package management service (NuGet, npm, Maven, Python) |
| Feed | Package registry in Azure Artifacts |
| Upstream Source | External package source proxied by Azure Artifacts |
| Coverage Threshold | Minimum code coverage threshold for the build to succeed |
| Playwright | Microsoft’s multi-browser E2E testing framework |
| GitVersion | Automatic semantic versioning tool based on Git history |
| Environment | Azure DevOps deployment target with checks and history |
| Deployment Job | Azure Pipelines Job type linked to an Environment |
| Template | Reusable YAML file across multiple pipelines |
| Matrix Strategy | Run the same Job with multiple configurations in parallel |
| PAT | Personal Access Token - Azure DevOps authentication token |
Search Terms
azure · repos · automation · pipelines · devops · iac · microsoft · pipeline · yaml · artifacts · merge · strategies · tests · .net · architecture · git · packages · playwright · pull · strategy · test