CI/CD Pipeline Mastery: GitHub Actions and Azure DevOps for Modern Development
Introduction
Continuous Integration and Continuous Deployment (CI/CD) pipelines automate the software delivery process from code commit to production deployment. This guide covers implementing robust CI/CD pipelines using GitHub Actions for lightweight workflows and Azure DevOps Pipelines for enterprise-scale deployments, including testing strategies, security scanning, and deployment patterns.
Why CI/CD Matters
CI/CD pipelines provide:
- Early Bug Detection: Automated tests catch issues before production
- Faster Releases: Eliminate manual deployment steps
- Consistency: Every deployment follows the same process
- Traceability: Complete audit trail of changes
- Confidence: Validated builds reduce deployment anxiety
GitHub Actions Fundamentals
Basic Workflow Structure
.github/workflows/ci.yml:
name: CI Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Restore dependencies
run: dotnet restore
- name: Build
run: dotnet build --configuration Release --no-restore
- name: Run tests
run: dotnet test --no-build --verbosity normal
Multi-Environment Workflows
Conditional Deployments:
name: Deploy to Environments
on:
push:
branches:
- main
- develop
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Build application
run: npm run build
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: webapp
path: dist/
deploy-dev:
needs: build
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
environment: development
steps:
- uses: actions/download-artifact@v3
with:
name: webapp
- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v2
with:
app-name: webapp-dev
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_DEV }}
package: .
deploy-prod:
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: production
url: https://app.contoso.com
steps:
- uses: actions/download-artifact@v3
with:
name: webapp
- name: Deploy to Azure Web App
uses: azure/webapps-deploy@v2
with:
app-name: webapp-prod
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE_PROD }}
package: .
Matrix Builds
Test Across Multiple Versions:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18.x, 20.x, 22.x]
exclude:
- os: macos-latest
node-version: 18.x
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
Reusable Workflows
.github/workflows/reusable-build.yml:
name: Reusable Build Workflow
on:
workflow_call:
inputs:
environment:
required: true
type: string
node-version:
required: false
type: string
default: '20'
secrets:
deploy-key:
required: true
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
- run: npm run build:${{ inputs.environment }}
- name: Deploy
env:
DEPLOY_KEY: ${{ secrets.deploy-key }}
run: ./deploy.sh
Call Reusable Workflow:
jobs:
deploy-dev:
uses: ./.github/workflows/reusable-build.yml
with:
environment: dev
node-version: '20'
secrets:
deploy-key: ${{ secrets.DEV_DEPLOY_KEY }}
Azure DevOps Pipelines
YAML Pipeline Structure
azure-pipelines.yml:
trigger:
branches:
include:
- main
- develop
paths:
include:
- src/*
exclude:
- docs/*
pool:
vmImage: 'ubuntu-latest'
variables:
buildConfiguration: 'Release'
dotnetVersion: '8.0.x'
stages:
- stage: Build
displayName: 'Build Application'
jobs:
- job: BuildJob
displayName: 'Build and Test'
steps:
- task: UseDotNet@2
displayName: 'Install .NET SDK'
inputs:
version: $(dotnetVersion)
- task: DotNetCoreCLI@2
displayName: 'Restore dependencies'
inputs:
command: 'restore'
projects: '**/*.csproj'
- task: DotNetCoreCLI@2
displayName: 'Build solution'
inputs:
command: 'build'
projects: '**/*.csproj'
arguments: '--configuration $(buildConfiguration)'
- task: DotNetCoreCLI@2
displayName: 'Run unit tests'
inputs:
command: 'test'
projects: '**/*Tests.csproj'
arguments: '--configuration $(buildConfiguration) --collect:"XPlat Code Coverage"'
- task: PublishCodeCoverageResults@1
displayName: 'Publish code coverage'
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '$(Agent.TempDirectory)/**/coverage.cobertura.xml'
- task: DotNetCoreCLI@2
displayName: 'Publish application'
inputs:
command: 'publish'
publishWebProjects: true
arguments: '--configuration $(buildConfiguration) --output $(Build.ArtifactStagingDirectory)'
- task: PublishBuildArtifacts@1
displayName: 'Publish artifacts'
inputs:
pathToPublish: '$(Build.ArtifactStagingDirectory)'
artifactName: 'drop'
- stage: Deploy_Dev
displayName: 'Deploy to Dev'
dependsOn: Build
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/develop'))
jobs:
- deployment: DeployDev
environment: 'development'
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
displayName: 'Deploy to Azure Web App'
inputs:
azureSubscription: 'Azure-Dev'
appName: 'webapp-dev'
package: '$(Pipeline.Workspace)/drop/**/*.zip'
- stage: Deploy_Prod
displayName: 'Deploy to Production'
dependsOn: Build
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployProd
environment: 'production'
strategy:
runOnce:
preDeploy:
steps:
- script: echo "Running pre-deployment checks"
deploy:
steps:
- task: AzureWebApp@1
displayName: 'Deploy to Azure Web App'
inputs:
azureSubscription: 'Azure-Prod'
appName: 'webapp-prod'
package: '$(Pipeline.Workspace)/drop/**/*.zip'
deploymentMethod: 'zipDeploy'
postDeploy:
steps:
- script: curl -f https://webapp-prod.azurewebsites.net/health || exit 1
displayName: 'Health check'
Pipeline Templates
templates/build-template.yml:
parameters:
- name: projectPath
type: string
- name: buildConfiguration
type: string
default: 'Release'
steps:
- task: UseDotNet@2
inputs:
version: '8.0.x'
- task: DotNetCoreCLI@2
displayName: 'Restore'
inputs:
command: 'restore'
projects: '${{ parameters.projectPath }}'
- task: DotNetCoreCLI@2
displayName: 'Build'
inputs:
command: 'build'
projects: '${{ parameters.projectPath }}'
arguments: '--configuration ${{ parameters.buildConfiguration }}'
Use Template:
stages:
- stage: Build
jobs:
- job: BuildAPI
steps:
- template: templates/build-template.yml
parameters:
projectPath: 'src/API/API.csproj'
buildConfiguration: 'Release'
- job: BuildWorker
steps:
- template: templates/build-template.yml
parameters:
projectPath: 'src/Worker/Worker.csproj'
Testing Strategies
Unit Tests in CI
GitHub Actions:
- name: Run unit tests with coverage
run: |
dotnet test \
--configuration Release \
--no-build \
--collect:"XPlat Code Coverage" \
--results-directory ./coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./coverage/**/coverage.cobertura.xml
fail_ci_if_error: true
Integration Tests
Docker Compose for Dependencies:
- name: Start dependencies
run: docker-compose -f docker-compose.test.yml up -d
- name: Wait for services
run: |
timeout 60 bash -c 'until docker exec postgres pg_isready; do sleep 1; done'
- name: Run integration tests
run: dotnet test IntegrationTests.csproj --filter Category=Integration
- name: Teardown
if: always()
run: docker-compose -f docker-compose.test.yml down
End-to-End Tests
Playwright E2E Tests:
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run E2E tests
run: npx playwright test
env:
BASE_URL: https://webapp-staging.azurewebsites.net
- name: Upload test results
if: always()
uses: actions/upload-artifact@v3
with:
name: playwright-report
path: playwright-report/
Security Scanning
GitHub Advanced Security
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: csharp, javascript
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
Dependency Scanning
- name: Run Snyk security scan
uses: snyk/actions/dotnet@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
Container Image Scanning
- name: Build Docker image
run: docker build -t myapp:${{ github.sha }} .
- name: Scan image with Trivy
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload results to GitHub Security
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
Deployment Patterns
Blue-Green Deployment
Azure DevOps:
- task: AzureAppServiceManage@0
displayName: 'Swap deployment slots'
inputs:
azureSubscription: 'Azure-Prod'
action: 'Swap Slots'
webAppName: 'webapp-prod'
resourceGroupName: 'rg-prod'
sourceSlot: 'staging'
targetSlot: 'production'
Canary Deployment
GitHub Actions with Traffic Splitting:
- name: Deploy canary (10% traffic)
uses: azure/webapps-deploy@v2
with:
app-name: webapp-prod
slot-name: canary
package: .
- name: Route 10% traffic to canary
run: |
az webapp traffic-routing set \
--resource-group rg-prod \
--name webapp-prod \
--distribution canary=10
- name: Monitor metrics
run: ./scripts/monitor-canary.sh
timeout-minutes: 30
- name: Promote canary or rollback
run: |
if [ "$CANARY_SUCCESS" = "true" ]; then
az webapp traffic-routing set --distribution canary=100
else
az webapp traffic-routing clear
fi
Pipeline Optimization
Caching Dependencies
GitHub Actions:
- name: Cache npm dependencies
uses: actions/cache@v3
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Cache NuGet packages
uses: actions/cache@v3
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
Azure DevOps:
- task: Cache@2
inputs:
key: 'nuget | "$(Agent.OS)" | **/packages.lock.json'
path: $(NUGET_PACKAGES)
displayName: 'Cache NuGet packages'
Parallel Jobs
GitHub Actions:
jobs:
test-unit:
runs-on: ubuntu-latest
steps: [...]
test-integration:
runs-on: ubuntu-latest
steps: [...]
lint:
runs-on: ubuntu-latest
steps: [...]
security-scan:
runs-on: ubuntu-latest
steps: [...]
Monitoring & Notifications
Slack Notifications
GitHub Actions:
- name: Notify Slack on failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "Build failed: ${{ github.repository }} - ${{ github.ref }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Application Insights Integration
Azure DevOps:
- task: AzureCLI@2
displayName: 'Track deployment'
inputs:
azureSubscription: 'Azure-Prod'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az monitor app-insights events custom \
--app webapp-prod-insights \
--event-type Deployment \
--properties '{"version":"$(Build.BuildNumber)","result":"success"}'
Best Practices
- Fail Fast: Run quick tests (linting, unit tests) before expensive operations
- Immutable Artifacts: Build once, deploy everywhere
- Environment Parity: Keep dev/staging/prod as similar as possible
- Secrets Management: Use GitHub Secrets or Azure Key Vault, never hardcode
- Version Control Everything: Pipelines, configuration, infrastructure
- Branch Protection: Require CI success before merging to main
- Deployment Gates: Manual approvals for production deployments
- Rollback Strategy: Automated rollback on health check failures
Troubleshooting
Pipeline Failures:
# GitHub Actions debug mode
# Add secret: ACTIONS_STEP_DEBUG = true
# Azure DevOps diagnostic logging
System.Debug: true
Intermittent Test Failures:
- Add retry logic for flaky tests
- Increase timeouts for network calls
- Use test isolation strategies
Key Takeaways
- GitHub Actions excels at simplicity and GitHub integration
- Azure DevOps provides enterprise features (approvals, gates, extensions)
- Automate security scanning in every pipeline run
- Cache dependencies to reduce build times
- Implement deployment strategies (blue-green, canary) for zero-downtime releases
Next Steps
- Explore GitHub Environments for deployment protection rules
- Implement Infrastructure as Code with Bicep or Terraform in pipelines
- Add smoke tests post-deployment for immediate validation
- Configure branch policies to enforce CI success
Additional Resources
- GitHub Actions Documentation
- Azure Pipelines Documentation
- YAML Pipeline Schema Reference
- GitHub Actions Marketplace
Automate everything, deploy with confidence.