GitHub Actions CD Pipeline — Build, Test and Deploy to Azure

The CI/CD pipeline is the automation that makes professional development possible — every code change goes through the same quality gates, every deployment is reproducible and auditable, and rollbacks take seconds rather than hours. The classified website’s pipeline implements blue-green deployment via Azure App Service slots: code is deployed to the staging slot, tested there, then swapped to production — the riskiest moment (the swap) takes milliseconds with instant rollback available.

GitHub Actions CI/CD Pipeline

// ── .github/workflows/deploy.yml ──────────────────────────────────────────
// name: Deploy Classified Website
// on:
//   push:
//     branches: [main]
//   workflow_dispatch:        # manual deployment trigger
//
// env:
//   DOTNET_VERSION: '8.0.x'
//   NODE_VERSION:   '20'
//   ACR_NAME:       classifiedappacr
//   APP_NAME:       classifiedapp-api
//   RG_NAME:        classifiedapp-rg
//
// jobs:
//
//   # ── Stage 1: Build + Unit Tests ──────────────────────────────────────
//   build-and-test:
//     runs-on: ubuntu-latest
//     steps:
//       - uses: actions/checkout@v4
//       - uses: actions/setup-dotnet@v4
//         with: { dotnet-version: '${{ env.DOTNET_VERSION }}' }
//       - run: dotnet restore
//       - run: dotnet build --no-restore
//       - run: dotnet test --filter Category=Unit --no-build
//           --collect:"XPlat Code Coverage"
//       - uses: codecov/codecov-action@v4
//
//   # ── Stage 2: Integration Tests ────────────────────────────────────────
//   integration-tests:
//     needs: build-and-test
//     runs-on: ubuntu-latest
//     services:
//       mssql:
//         image: mcr.microsoft.com/mssql/server:2022-latest
//         env: { SA_PASSWORD: TestPass1234!, ACCEPT_EULA: Y }
//         ports: ['1433:1433']
//     steps:
//       - uses: actions/checkout@v4
//       - uses: actions/setup-dotnet@v4
//       - run: dotnet test --filter Category=Integration
//         env:
//           ConnectionStrings__Default: >
//             Server=localhost,1433;Database=ClassifiedTest;
//             User Id=sa;Password=TestPass1234!;Encrypt=false
//
//   # ── Stage 3: Build Docker image ────────────────────────────────────────
//   build-image:
//     needs: integration-tests
//     runs-on: ubuntu-latest
//     outputs:
//       image-tag: ${{ steps.meta.outputs.tags }}
//     steps:
//       - uses: actions/checkout@v4
//       - uses: azure/login@v2
//         with: { creds: '${{ secrets.AZURE_CREDENTIALS }}' }
//       - name: Build and push to ACR
//         run: |
//           az acr login --name $ACR_NAME
//           docker build -t $ACR_NAME.azurecr.io/classifiedapi:$GITHUB_SHA .
//           docker push $ACR_NAME.azurecr.io/classifiedapi:$GITHUB_SHA
//
//   # ── Stage 4: Deploy to Staging Slot ────────────────────────────────────
//   deploy-staging:
//     needs: build-image
//     runs-on: ubuntu-latest
//     environment: staging       # GitHub Environment with staging secrets
//     steps:
//       - uses: azure/login@v2
//         with: { creds: '${{ secrets.AZURE_CREDENTIALS }}' }
//       - name: Apply EF Core migrations
//         run: |
//           dotnet tool install -g dotnet-ef
//           dotnet ef database update \
//             --connection "${{ secrets.STAGING_DB_CONNECTION }}"
//       - name: Deploy to staging slot
//         uses: azure/webapps-deploy@v3
//         with:
//           app-name:  ${{ env.APP_NAME }}
//           slot-name: staging
//           images:    '${{ env.ACR_NAME }}.azurecr.io/classifiedapi:${{ github.sha }}'
//
//   # ── Stage 5: Smoke Tests on Staging ────────────────────────────────────
//   smoke-tests:
//     needs: deploy-staging
//     runs-on: ubuntu-latest
//     steps:
//       - uses: actions/checkout@v4
//       - name: Run smoke tests
//         run: |
//           # Health check
//           curl -f https://classifiedapp-api-staging.azurewebsites.net/api/health
//           # Search returns results
//           curl -f "https://classifiedapp-api-staging.azurewebsites.net/api/listings?pageSize=1"
//           # Auth endpoint responds
//           curl -f -X POST https://classifiedapp-api-staging.azurewebsites.net/api/auth/login \
//                -H "Content-Type: application/json" \
//                -d '{"email":"wrong@test.com","password":"wrong"}' \
//                -w "%{http_code}" | grep 401
//
//   # ── Stage 6: Swap Staging → Production ─────────────────────────────────
//   swap-to-production:
//     needs: smoke-tests
//     runs-on: ubuntu-latest
//     environment: production    # requires manual approval in GitHub
//     steps:
//       - uses: azure/login@v2
//         with: { creds: '${{ secrets.AZURE_CREDENTIALS }}' }
//       - name: Swap slots
//         run: |
//           az webapp deployment slot swap \
//             --resource-group $RG_NAME \
//             --name $APP_NAME \
//             --slot staging \
//             --target-slot production
//
//   # ── Stage 7: Build and deploy Angular ──────────────────────────────────
//   deploy-frontend:
//     needs: swap-to-production
//     runs-on: ubuntu-latest
//     steps:
//       - uses: actions/checkout@v4
//       - uses: actions/setup-node@v4
//         with: { node-version: '${{ env.NODE_VERSION }}' }
//       - run: npm ci && npm run build -- --configuration=production
//       - uses: Azure/static-web-apps-deploy@v1
//         with:
//           azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_TOKEN }}
//           action: upload
//           app_location: /
//           output_location: dist/classified-app/browser
Note: The environment: production on the swap-to-production job enables GitHub’s environment protection rules — you can require manual approval from a designated reviewer before the swap happens. This is the production gate: automated tests ran on staging, a human reviews the staging environment, clicks “Approve,” and the swap proceeds. Combined with the staging slot smoke tests, this gives confidence that only working code reaches production.
Tip: The EF Core migration step runs in the deploy-staging job — before the new API version starts. This ensures the database schema is updated before any API instance (staging or production) starts using the new code. The migration must be backward-compatible with the current production API (which will briefly run against the new schema during the slot swap). If the migration requires application downtime, implement it as a three-phase deploy: add nullable column (backward compatible), deploy new code, make column non-nullable in a follow-up migration.
Warning: The slot swap sends production traffic to the new code instantly — but App Service instances that were warming up in staging now serve production. Ensure the staging slot’s WEBSITE_OVERRIDE_STICKY_EXTENSION_VERSIONS setting is configured so the production slot’s app settings (database connections, API keys) are swapped correctly. Sticky settings (marked “deployment slot setting”) stay with the slot during swap; non-sticky settings are swapped with the code. Verify which settings should be sticky vs swappable before your first production deployment.

Common Mistakes

Mistake 1 — Deploying directly to production without a staging slot (no rollback)

❌ Wrong — deploy to production directly; bad deploy causes downtime; rollback requires re-deploying previous code (minutes of downtime).

✅ Correct — deploy to staging → smoke test → swap; rollback is another swap (seconds, zero downtime).

Mistake 2 — Running migrations after deployment instead of before (new API + old schema)

❌ Wrong — deploy new API, then run migrations; new API starts hitting old schema; errors during migration window.

✅ Correct — run backward-compatible migrations before deployment; schema ready for new code before it starts.

🧠 Test Yourself

The smoke tests pass on staging. The swap-to-production job requires manual approval. An approver notices the staging site has a visual bug in the listing card. What is the fastest fix?