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
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.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.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.