Tổng quan Pipeline Stages
Một CI/CD pipeline hoàn chỉnh cho .NET project thường gồm các stage:
| Stage | Mục đích | Fail fast? |
|---|---|---|
| Build | Restore, compile, check warnings-as-errors | Yes |
| Test | Unit test, integration test, code coverage | Yes |
| Publish | Tạo artifact (NuGet, Docker image, zip) | Yes |
| Deploy Staging | Deploy lên staging environment | Yes |
| Approval | Manual approval hoặc automated gate | Block |
| Deploy Production | Deploy lên production | - |
GitHub Actions vs Azure DevOps Pipelines
| Tiêu chí | GitHub Actions | Azure DevOps Pipelines |
|---|---|---|
| YAML syntax | Đơn giản, dễ đọc | Phức tạp hơn, nhiều feature |
| Self-hosted runner | Dễ setup | Agent pool quản lý tốt |
| Marketplace | 15k+ actions | Task extensions phong phú |
| Environment protection | Environment secrets + rules | Approval gates + checks |
| Parallel jobs | Free tier: 20 concurrent | Free tier: 1 parallel job |
| Cache | actions/cache | Pipeline caching built-in |
| Artifact retention | 90 ngày (default) | Tuỳ chỉnh theo project |
| Tích hợp Azure | Cần setup OIDC/secret | Native 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)
yamlname: .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
| Strategy | Key pattern | Hiệu quả |
|---|---|---|
| Exact match | nuget-${{ hashFiles('**/*.csproj') }} | Tốt nhất khi ít thay đổi package |
| Prefix fallback | restore-keys: nuget-${{ runner.os }}- | Dùng cache cũ khi có package mới |
| Lock file | nuget-${{ 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ể.