Learnixo
Back to blog
AI Systemsintermediate

GitHub Actions Deployment — CD Pipelines to Azure

Deploy .NET applications to Azure with GitHub Actions: Blue/Green deployment to App Service, deployment slots, environment approval gates, rollback strategies, and production deployment workflows.

Asma Hafeez KhanMay 16, 20265 min read
GitHub ActionsCI/CDAzureDeployment.NET
Share:š•

Complete CD Workflow

YAML
# .github/workflows/cd.yml
# Triggers on merge to main — build, test, deploy to staging, then await approval for production

name: CD

on:
  push:
    branches: [main]

permissions:
  id-token: write  # OIDC
  contents: read

env:
  DOTNET_VERSION: '8.x'
  APP_NAME: 'clinical-platform'
  RESOURCE_GROUP: 'clinical-rg'

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}

    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}

      - name: Build and publish
        run: |
          dotnet publish src/Host/SystemForge.Api/SystemForge.Api.csproj \
            --configuration Release \
            --output ./publish

      - name: Upload artifact
        uses: actions/upload-artifact@v4
        with:
          name: app-publish
          path: ./publish

Staging Deployment

YAML
  deploy-staging:
    runs-on: ubuntu-latest
    needs: build
    environment: staging

    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: app-publish
          path: ./publish

      - name: Azure login (OIDC)
        uses: azure/login@v2
        with:
          client-id:       ${{ vars.AZURE_CLIENT_ID }}
          tenant-id:       ${{ vars.AZURE_TENANT_ID }}
          subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

      - name: Deploy to staging slot
        uses: azure/webapps-deploy@v3
        with:
          app-name:  ${{ env.APP_NAME }}
          slot-name: staging
          package:   ./publish

      - name: Run smoke tests against staging
        run: |
          # Wait for App Service to become healthy
          for i in $(seq 1 12); do
            STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
              "https://${{ env.APP_NAME }}-staging.azurewebsites.net/health/live")
            if [ "$STATUS" = "200" ]; then
              echo "Staging is healthy"
              break
            fi
            echo "Waiting... HTTP $STATUS"
            sleep 10
          done

          # Run a functional smoke test
          curl -f \
            "https://${{ env.APP_NAME }}-staging.azurewebsites.net/api/health" \
            -H "Accept: application/json"

Production Deployment with Approval Gate

YAML
  deploy-production:
    runs-on: ubuntu-latest
    needs: deploy-staging
    environment: production  # requires reviewer approval in GitHub Environments

    steps:
      - name: Azure login (OIDC)
        uses: azure/login@v2
        with:
          client-id:       ${{ vars.AZURE_CLIENT_ID }}
          tenant-id:       ${{ vars.AZURE_TENANT_ID }}
          subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

      - name: Swap staging → production
        uses: azure/cli@v2
        with:
          inlineScript: |
            az webapp deployment slot swap \
              --resource-group ${{ env.RESOURCE_GROUP }} \
              --name ${{ env.APP_NAME }} \
              --slot staging \
              --target-slot production

      - name: Verify production health
        run: |
          for i in $(seq 1 10); do
            STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
              "https://${{ env.APP_NAME }}.azurewebsites.net/health/live")
            if [ "$STATUS" = "200" ]; then
              echo "Production is healthy — deployment successful"
              exit 0
            fi
            echo "Attempt $i: HTTP $STATUS"
            sleep 15
          done
          echo "Production health check failed after deployment"
          exit 1

Rollback on Failure

YAML
  rollback-on-failure:
    runs-on: ubuntu-latest
    needs: deploy-production
    if: failure()  # only runs if deploy-production fails

    steps:
      - name: Azure login (OIDC)
        uses: azure/login@v2
        with:
          client-id:       ${{ vars.AZURE_CLIENT_ID }}
          tenant-id:       ${{ vars.AZURE_TENANT_ID }}
          subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

      - name: Swap back (rollback)
        uses: azure/cli@v2
        with:
          inlineScript: |
            # Swap production back to staging (reverses the swap)
            az webapp deployment slot swap \
              --resource-group ${{ env.RESOURCE_GROUP }} \
              --name ${{ env.APP_NAME }} \
              --slot production \
              --target-slot staging
            echo "Rollback complete — previous version restored to production"

      - name: Notify team of rollback
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "Production deployment ROLLED BACK for ${{ github.repository }}. Commit: ${{ github.sha }}"
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Database Migration as a Deployment Step

YAML
# Run EF Core migrations BEFORE swapping to production
# Migrations must be backward-compatible with the current production code

  run-migrations:
    runs-on: ubuntu-latest
    needs: build
    environment: production  # uses production connection string

    steps:
      - uses: actions/checkout@v4

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}

      - name: Install EF Core tools
        run: dotnet tool install --global dotnet-ef

      - name: Run migrations
        run: |
          dotnet ef database update \
            --project src/Modules/Prescriptions/Prescriptions.Infrastructure \
            --startup-project src/Host/SystemForge.Api \
            --context PrescriptionsDbContext
        env:
          ConnectionStrings__Clinical: ${{ secrets.PRODUCTION_CONNECTION_STRING }}

# Run migrations before the slot swap — new schema is in place before new code goes live
# If migration fails, abort before the swap (production code still runs on old schema)

Deployment Notifications

YAML
      - name: Notify Slack on success
        if: success()
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "Deployed to production: ${{ github.repository }}@${{ github.sha }}",
              "attachments": [{
                "color": "good",
                "text": "Commit: ${{ github.event.head_commit.message }}\nAuthor: ${{ github.actor }}"
              }]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

Production issue I've seen: A team's CD pipeline deployed new code to production before running database migrations. For 3 minutes, the new application code was talking to the old schema. A query that referenced a new column (warfarin_enhanced_check_required) failed for every prescription that was accessed during those 3 minutes — nurses received 500 errors. The migration then ran and everything worked. The fix: always run migrations first, then deploy the application code. For zero-downtime migrations: make the migration backward-compatible so the old code still works with the new schema (add nullable columns, don't remove or rename).


Key Takeaway

Structure the CD pipeline as: build → deploy staging → smoke test → await approval → swap to production → verify health → rollback on failure. Use OIDC for Azure authentication — no stored credentials. Protect the production environment in GitHub with required reviewers. Run database migrations before the slot swap, not after. Always include a rollback step triggered on failure. Notify the team on both success and failure via Slack or Teams integration.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.