Home / Developer Tools / CI/CD Pipeline Mastery: GitHub Actions and Azure DevOps for Modern Development
Developer Tools

CI/CD Pipeline Mastery: GitHub Actions and Azure DevOps for Modern Development

Continuous Integration and Continuous Deployment (CI/CD) pipelines automate the software delivery process from code commit to production deployment.

What you will learn

Practical execution with concise explanations, real implementation patterns, and production-ready recommendations.

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:**

```yaml
name: Deploy to Environments

on:
  push:
```yaml
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:**

```yaml
jobs:
  test:
```yaml
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`:**

```yaml
name: Reusable Build Workflow

on:
  workflow_call:
```yaml
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:**

```yaml
jobs:
  deploy-dev:
```yaml
uses: ./.github/workflows/reusable-build.yml
with:
  environment: dev
  node-version: '20'
secrets:
  deploy-key: ${{ secrets.DEV_DEPLOY_KEY }}

## Azure DevOps Pipelines

![Azure DevOps Pipelines](/images/articles/developer-tools/2025-03-10-cicd-pipeline-mastery-github-actions-azure-devops-modern-development-ctx-1.svg)

### YAML Pipeline Structure





**`azure-pipelines.yml`:**

```yaml
trigger:
  branches:
```yaml
include:
  - main
  - develop```
  paths:
```yaml
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`:**

```yaml
parameters:
  - name: projectPath
```yaml
type: string```
  - name: buildConfiguration
```yaml
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:**

```yaml
stages:
  - stage: Build
```yaml
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:**

```yaml
- name: Run unit tests with coverage
  run: |
```text
dotnet test \
  --configuration Release \
  --no-build \
  --collect:"XPlat Code Coverage" \
  --results-directory ./coverage

Expected output:

Passed!  - Failed: 0, Passed: 24, Skipped: 0, Total: 24, Duration: 1.8 s

Terminal output for dotnet test

  • 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:**

```yaml
- name: Start dependencies
  run: docker-compose -f docker-compose.test.yml up -d

- name: Wait for services
  run: |
```text
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:**

```yaml
- name: Install Playwright
  run: npx playwright install --with-deps

- name: Run E2E tests
  run: npx playwright test
  env:
```yaml
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

![Security Scanning](/images/articles/developer-tools/2025-03-10-cicd-pipeline-mastery-github-actions-azure-devops-modern-development-ctx-2.svg)

### GitHub Advanced Security





```yaml
- name: Initialize CodeQL
  uses: github/codeql-action/init@v3
  with:
```yaml
languages: csharp, javascript
  • name: Autobuild uses: github/codeql-action/autobuild@v3

  • name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3


### Dependency Scanning

```yaml
- name: Run Snyk security scan
  uses: snyk/actions/dotnet@master
  env:
```yaml
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}```
  with:
```yaml
args: --severity-threshold=high

### Container Image Scanning

```yaml
- name: Build Docker image
  run: docker build -t myapp:${{ github.sha }} .

- name: Scan image with Trivy
  uses: aquasecurity/trivy-action@master
  with:
```yaml
image-ref: myapp:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'

Expected output:

[+] Building 24.3s (12/12) FINISHED
 => naming to docker.io/library/myapp:latest

Terminal output for docker build

  • 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:**

```yaml
- task: AzureAppServiceManage@0
  displayName: 'Swap deployment slots'
  inputs:
```yaml
azureSubscription: 'Azure-Prod'
action: 'Swap Slots'
webAppName: 'webapp-prod'
resourceGroupName: 'rg-prod'
sourceSlot: 'staging'
targetSlot: 'production'

### Canary Deployment

**GitHub Actions with Traffic Splitting:**

```yaml
- name: Deploy canary (10% traffic)
  uses: azure/webapps-deploy@v2
  with:
```yaml
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

![Pipeline Optimization](/images/articles/developer-tools/2025-03-10-cicd-pipeline-mastery-github-actions-azure-devops-modern-development-ctx-3.svg)

### Caching Dependencies





**GitHub Actions:**

```yaml
- name: Cache npm dependencies
  uses: actions/cache@v3
  with:
```yaml
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:**

```yaml
- task: Cache@2
  inputs:
```yaml
key: 'nuget | "$(Agent.OS)" | **/packages.lock.json'
path: $(NUGET_PACKAGES)```
  displayName: 'Cache NuGet packages'

Parallel Jobs

GitHub Actions:

jobs:
  test-unit:
```yaml
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:**

```yaml
- name: Notify Slack on failure
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
```yaml
payload: |
  {
    "text": "Build failed: ${{ github.repository }} - ${{ github.ref }}"
  }```
  env:
```yaml
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

### Application Insights Integration

**Azure DevOps:**

```yaml
- task: AzureCLI@2
  displayName: 'Track deployment'
  inputs:
```yaml
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

1. **Fail Fast**: Run quick tests (linting, unit tests) before expensive operations
2. **Immutable Artifacts**: Build once, deploy everywhere
3. **Environment Parity**: Keep dev/staging/prod as similar as possible
4. **Secrets Management**: Use GitHub Secrets or Azure Key Vault, never hardcode
5. **Version Control Everything**: Pipelines, configuration, infrastructure
6. **Branch Protection**: Require CI success before merging to main
7. **Deployment Gates**: Manual approvals for production deployments
8. **Rollback Strategy**: Automated rollback on health check failures





## Troubleshooting

**Pipeline Failures:**





```bash
# 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

Architecture Decision and Tradeoffs

When designing development workflow solutions with Developer Tools, consider these key architectural trade-offs:

Approach Best For Tradeoff
Managed / platform service Rapid delivery, reduced ops burden Less customisation, potential vendor lock-in
Custom / self-hosted Full control, advanced tuning Higher operational overhead and cost

Recommendation: Start with the managed approach for most workloads and move to custom only when specific requirements demand it.

Validation and Versioning

  • Last validated: April 2026
  • Validate examples against your tenant, region, and SKU constraints before production rollout.
  • Keep module, CLI, and SDK versions pinned in automation pipelines and review quarterly.

Security and Governance Considerations

  • Apply least-privilege access using RBAC roles and just-in-time elevation for admin tasks.
  • Store secrets in managed secret stores and avoid embedding credentials in scripts or source files.
  • Enable audit logging, data protection policies, and periodic access reviews for regulated workloads.

Cost and Performance Notes

  • Define budgets and alerts, then monitor usage and cost trends continuously after go-live.
  • Baseline performance with synthetic and real-user checks before and after major changes.
  • Scale resources with measured thresholds and revisit sizing after usage pattern changes.

Official Microsoft References

  • https://learn.microsoft.com/visualstudio/
  • https://learn.microsoft.com/azure/devops/
  • https://learn.microsoft.com/github/

Public Examples from Official Sources

  • These examples are sourced from official public Microsoft documentation and sample repositories.
  • Documentation examples: https://learn.microsoft.com/visualstudio/
  • Sample repositories: https://github.com/microsoft/vscode-extension-samples
  • Prefer adapting these examples to your tenant, subscriptions, and governance requirements before production use.

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


Automate everything, deploy with confidence.

Discussion