CI/CD
Core Principles
- Pipeline as code — YAML pipelines committed to the repo. No click-ops in the UI.
- Fast feedback — Build and test on every push. Cache NuGet packages. Fail fast.
- Build once, deploy many — Build the artifact once, promote it through environments (dev → staging → production).
- Never skip tests — Tests gate the pipeline. No deployment without passing tests.
Patterns
GitHub Actions — Build + Test
yaml# .github/workflows/ci.yml name: CI on: push: branches: [main] pull_request: branches: [main] env: DOTNET_VERSION: '10.0.x' DOTNET_NOLOGO: true DOTNET_CLI_TELEMETRY_OPTOUT: true jobs: build-and-test: runs-on: ubuntu-latest services: postgres: image: postgres:18 env: POSTGRES_DB: testdb POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - uses: actions/checkout@v5 - name: Setup .NET uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ env.DOTNET_VERSION }} - name: Restore run: dotnet restore - name: Build run: dotnet build --no-restore --configuration Release - name: Format check run: dotnet format --verify-no-changes --no-restore - name: Test run: dotnet test --no-build --configuration Release --logger trx --results-directory TestResults env: ConnectionStrings__Default: "Host=localhost;Database=testdb;Username=postgres;Password=postgres" - name: Publish test results uses: actions/upload-artifact@v5 if: always() with: name: test-results path: TestResults/*.trx
GitHub Actions — Build + Publish Docker Image
yaml# .github/workflows/publish.yml name: Publish on: push: tags: ['v*'] jobs: publish: runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v5 - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Extract version from tag id: version run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT - name: Build and push uses: docker/build-push-action@v6 with: context: . push: true tags: | ghcr.io/${{ github.repository }}:${{ steps.version.outputs.VERSION }} ghcr.io/${{ github.repository }}:latest
Azure DevOps — Build + Test
Same restore → build → format → test flow as GitHub Actions. Key differences:
yaml# azure-pipelines.yml trigger: branches: include: [main] paths: exclude: ['*.md', docs/] pool: vmImage: 'ubuntu-latest' # vs runs-on: ubuntu-latest variables: dotnetVersion: '10.0.x' # Key task differences from GitHub Actions: # Setup .NET: task: UseDotNet@2 (inputs: version: $(dotnetVersion)) # Test results: task: PublishTestResults@2 (testResultsFormat: VSTest) # Steps use `script:` + `displayName:` instead of `- name:` + `run:` # Services (e.g., Postgres) require a separate Docker task or pipeline service connection
NuGet Package Publishing
yaml# Part of GitHub Actions workflow - name: Pack run: dotnet pack src/MyLibrary -c Release -o ./nupkg --no-build - name: Push to NuGet run: dotnet nuget push ./nupkg/*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json
Anti-patterns
Don't Build Different Artifacts per Environment
yaml# BAD — building separately for each environment - script: dotnet publish -c Debug # for dev - script: dotnet publish -c Release # for prod # GOOD — build once, deploy everywhere - script: dotnet publish -c Release -o ./publish # Then deploy the same ./publish artifact to dev, staging, prod
Don't Skip Format Checks in CI
yaml# BAD — no format enforcement steps: - run: dotnet build - run: dotnet test # GOOD — format check catches style issues early steps: - run: dotnet build - run: dotnet format --verify-no-changes - run: dotnet test
Don't Hardcode Secrets in Pipelines
yaml# BAD — secret in pipeline YAML env: DB_PASSWORD: "my-secret-password" # GOOD — use pipeline secrets env: DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
Decision Guide
| Scenario | Recommendation |
|---|---|
| Open source project | GitHub Actions |
| Enterprise with Azure | Azure DevOps Pipelines |
| Docker deployment | Multi-stage build in CI, push to container registry |
| NuGet library | Build → Test → Pack → Push on tag |
| Database migrations | Run in CI test stage, script for production |
| Environment promotion | Same artifact, different configuration |

