EngineeringSolution ArchitectPlatform and DevOps

Session 01 - CI/CD Pipeline Design

Tổng quan Pipeline Stages

Một CI/CD pipeline hoàn chỉnh cho .NET project thường gồm các stage:

StageMục đíchFail fast?
BuildRestore, compile, check warnings-as-errorsYes
TestUnit test, integration test, code coverageYes
PublishTạo artifact (NuGet, Docker image, zip)Yes
Deploy StagingDeploy lên staging environmentYes
ApprovalManual approval hoặc automated gateBlock
Deploy ProductionDeploy lên production-

GitHub Actions vs Azure DevOps Pipelines

Tiêu chíGitHub ActionsAzure DevOps Pipelines
YAML syntaxĐơn giản, dễ đọcPhức tạp hơn, nhiều feature
Self-hosted runnerDễ setupAgent pool quản lý tốt
Marketplace15k+ actionsTask extensions phong phú
Environment protectionEnvironment secrets + rulesApproval gates + checks
Parallel jobsFree tier: 20 concurrentFree tier: 1 parallel job
Cacheactions/cachePipeline caching built-in
Artifact retention90 ngày (default)Tuỳ chỉnh theo project
Tích hợp AzureCần setup OIDC/secretNative integration

Trade-off: GitHub Actions phù hợp team dùng GitHub ecosystem, setup nhanh. Azure DevOps mạnh hơn cho enterprise với approval workflows phức tạp, test plan integration.

Multi-stage Pipeline - GitHub Actions (Complete YAML)

yaml
name: .NET CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  DOTNET_VERSION: '8.0.x'
  SOLUTION_PATH: 'src/MyApp.sln'
  PUBLISH_PROJECT: 'src/MyApp.Api/MyApp.Api.csproj'

# Chỉ cho phép 1 deployment cùng lúc per environment
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

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

      # Cache NuGet packages - tiết kiệm 30-60s mỗi build
      - name: Cache NuGet
        uses: actions/cache@v4
        with:
          path: ~/.nuget/packages
          key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }}
          restore-keys: nuget-${{ runner.os }}-

      - name: Restore
        run: dotnet restore ${{ env.SOLUTION_PATH }}

      - name: Build
        run: dotnet build ${{ env.SOLUTION_PATH }} --no-restore -c Release -warnaserror

      - name: Upload build output
        uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: src/**/bin/Release/**/
          retention-days: 1

  test:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}

      - name: Cache NuGet
        uses: actions/cache@v4
        with:
          path: ~/.nuget/packages
          key: nuget-${{ runner.os }}-${{ hashFiles('**/*.csproj') }}

      - name: Run Tests
        run: |
          dotnet test ${{ env.SOLUTION_PATH }} \
            --no-restore \
            -c Release \
            --logger "trx;LogFileName=results.trx" \
            --collect:"XPlat Code Coverage" \
            -- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=cobertura

      - name: Publish Test Results
        uses: dorny/test-reporter@v1
        if: always()
        with:
          name: Test Results
          path: '**/*.trx'
          reporter: dotnet-trx

      - name: Code Coverage Report
        uses: irongut/CodeCoverageSummary@v1.3.0
        with:
          filename: '**/coverage.cobertura.xml'
          fail_below_min: true
          thresholds: '60 80'

  publish:
    needs: test
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.version.outputs.version }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}

      - name: Generate Version
        id: version
        run: echo "version=1.0.${{ github.run_number }}" >> $GITHUB_OUTPUT

      - name: Publish
        run: |
          dotnet publish ${{ env.PUBLISH_PROJECT }} \
            -c Release \
            -o ./publish \
            /p:Version=${{ steps.version.outputs.version }}

      - name: Upload Artifact
        uses: actions/upload-artifact@v4
        with:
          name: app-${{ steps.version.outputs.version }}
          path: ./publish
          retention-days: 30

  deploy-staging:
    needs: publish
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: https://myapp-staging.azurewebsites.net
    steps:
      - name: Download Artifact
        uses: actions/download-artifact@v4
        with:
          name: app-${{ needs.publish.outputs.version }}
          path: ./publish

      - name: Login to Azure
        uses: azure/login@v2
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Deploy to Staging
        uses: azure/webapps-deploy@v3
        with:
          app-name: myapp-staging
          package: ./publish

  deploy-production:
    needs: [publish, deploy-staging]
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://myapp.azurewebsites.net
    steps:
      - name: Download Artifact
        uses: actions/download-artifact@v4
        with:
          name: app-${{ needs.publish.outputs.version }}
          path: ./publish

      - name: Login to Azure
        uses: azure/login@v2
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      - name: Deploy to Production
        uses: azure/webapps-deploy@v3
        with:
          app-name: myapp-production
          package: ./publish

Branch Policies

Cấu hình branch protection cho main:

  • Require pull request - tối thiểu 1 reviewer
  • Require status checks - build + test phải pass
  • Require linear history - squash merge
  • No force push - bảo vệ commit history
yaml
# Cách enforce trong GitHub Actions - chỉ deploy khi qua PR
on:
  push:
    branches: [main]  # Chỉ trigger sau khi merge PR

Cache Strategies cho NuGet

StrategyKey patternHiệu quả
Exact matchnuget-${{ hashFiles('**/*.csproj') }}Tốt nhất khi ít thay đổi package
Prefix fallbackrestore-keys: nuget-${{ runner.os }}-Dùng cache cũ khi có package mới
Lock filenuget-${{ hashFiles('packages.lock.json') }}Chính xác nhất, cần enable lock file

Enable NuGet lock file:

xml
<!-- Directory.Build.props -->
<PropertyGroup>
  <RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>

Parallel Jobs - Tối ưu thời gian

yaml
# Chạy unit test và integration test song song
test-unit:
  needs: build
  runs-on: ubuntu-latest
  steps:
    - run: dotnet test --filter "Category=Unit"

test-integration:
  needs: build
  runs-on: ubuntu-latest
  services:
    sqlserver:
      image: mcr.microsoft.com/mssql/server:2022-latest
      env:
        SA_PASSWORD: Test@12345
        ACCEPT_EULA: Y
      ports:
        - 1433:1433
  steps:
    - run: dotnet test --filter "Category=Integration"

Tip: Với monorepo, dùng paths filter để chỉ build project bị thay đổi, giảm thời gian pipeline đáng kể.