ABP VNext + GitHub Actions:CI/CD 全流程自动化
🌟 ABP VNext + GitHub Actions:CI/CD 全流程自动化
📚 目录
- 🌟 ABP VNext + GitHub Actions:CI/CD 全流程自动化
-
- 🤩 TL;DR
- 🔄 全局流程概览
- 1️⃣ 准备工作与项目结构
-
- 1.1 🛠️ 工具链与 Secrets
- 1.2 📁 项目目录示例
- 2️⃣ 🔨 Build & Test(并行编译与单测)
-
-
- 🔄 子流程图
-
- 3️⃣ 🕵️ Static Analysis(SonarCloud & CodeQL)
-
-
- 🔄 子流程图
-
- 4️⃣ 📦 Package & Publish(NuGet 与 Docker)
- 5️⃣ 🚀 Deploy to Staging(预发布环境)
- 6️⃣ 🏭 Deploy to Production(生产环境)
- 7️⃣ ⏪ Rollback & Alert(自动回滚与告警)
-
- Rollback Staging
- Rollback Production
- 🔧 ABP VNext 专属集成示例
-
- Program.cs 示例
- Helm Chart 探针示例 (`charts/myapp/values.yaml`)
- 📦 附录:配置文件
-
- sonar-project.properties
- CodeQL 配置 (`.github/codeql/codeql-config.yml`)
🤩 TL;DR
- 🚀 端到端流水线:Push → 并行编译/测试 → 静态扫描 (SonarCloud/CodeQL) → NuGet/Docker 打包 → 分环境部署 → 自动回滚
- 🔒 严格审批:在 GitHub Environments 中分别为
staging
与production
配置 Required Reviewers - ⚡ 性能优化:NuGet 缓存、actions/cache、Docker Layer 缓存、并行 Jobs、Concurrency 控制
- 🛠️ 深度契合 ABP VNext:自动执行 EF Core 迁移、Swagger/UI、Health Checks 与 AKS 探针
🔄 全局流程概览
#mermaid-svg-nrOGCOuXNVMxqyYz {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-nrOGCOuXNVMxqyYz .error-icon{fill:#552222;}#mermaid-svg-nrOGCOuXNVMxqyYz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nrOGCOuXNVMxqyYz .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-nrOGCOuXNVMxqyYz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nrOGCOuXNVMxqyYz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nrOGCOuXNVMxqyYz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nrOGCOuXNVMxqyYz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nrOGCOuXNVMxqyYz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nrOGCOuXNVMxqyYz .marker.cross{stroke:#333333;}#mermaid-svg-nrOGCOuXNVMxqyYz svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nrOGCOuXNVMxqyYz .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nrOGCOuXNVMxqyYz .cluster-label text{fill:#333;}#mermaid-svg-nrOGCOuXNVMxqyYz .cluster-label span{color:#333;}#mermaid-svg-nrOGCOuXNVMxqyYz .label text,#mermaid-svg-nrOGCOuXNVMxqyYz span{fill:#333;color:#333;}#mermaid-svg-nrOGCOuXNVMxqyYz .node rect,#mermaid-svg-nrOGCOuXNVMxqyYz .node circle,#mermaid-svg-nrOGCOuXNVMxqyYz .node ellipse,#mermaid-svg-nrOGCOuXNVMxqyYz .node polygon,#mermaid-svg-nrOGCOuXNVMxqyYz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nrOGCOuXNVMxqyYz .node .label{text-align:center;}#mermaid-svg-nrOGCOuXNVMxqyYz .node.clickable{cursor:pointer;}#mermaid-svg-nrOGCOuXNVMxqyYz .arrowheadPath{fill:#333333;}#mermaid-svg-nrOGCOuXNVMxqyYz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nrOGCOuXNVMxqyYz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nrOGCOuXNVMxqyYz .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-nrOGCOuXNVMxqyYz .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-nrOGCOuXNVMxqyYz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nrOGCOuXNVMxqyYz .cluster text{fill:#333;}#mermaid-svg-nrOGCOuXNVMxqyYz .cluster span{color:#333;}#mermaid-svg-nrOGCOuXNVMxqyYz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nrOGCOuXNVMxqyYz :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} Yes No Yes No 🛎️ Push 代码 🔨 Build & Test 🕵️ Static Scan 📦 Package & Publish 🚀 Deploy to Staging ✅ Staging OK? 🏭 Deploy to Production ⏪ Rollback Staging ✅ Production OK? 🎉 完成 ⏪ Rollback Production
1️⃣ 准备工作与项目结构
1.1 🛠️ 工具链与 Secrets
在仓库 Settings → Secrets 添加以下凭据:
AZURE_CREDENTIALS
:Azure Service Principal JSON(az ad sp create-for-rbac … --sdk-auth
)NUGET_API_KEY
:NuGet.org 发布 KeySONAR_TOKEN
:SonarCloud Access TokenSLACK_WEBHOOK_URL
:Slack Incoming Webhook URLGITHUB_TOKEN
(Actions 内置,用于 GHCR)
🎯 示例 CLI:
az ad sp create-for-rbac \\ --name \"abp-ci-sp\" \\ --role contributor \\ --scopes /subscriptions/<SUB_ID>/resourceGroups/<RG_NAME> \\ --sdk-auth > azure-credentials.json
1.2 📁 项目目录示例
.├─ .github/workflows/ci-cd.yml├─ src/│ ├─ MyApp.Domain/│ ├─ MyApp.Application/│ ├─ MyApp.EntityFrameworkCore/│ └─ MyApp.HttpApi.Host/└─ tests/ └─ MyApp.Tests/
2️⃣ 🔨 Build & Test(并行编译与单测)
📝 本 Job 目标:并行 Restore/Build/Test,上传测试报告
build-test: name: 🔨 Build & Test runs-on: ubuntu-latest strategy: matrix: dotnet-version: [\'8.0.x\'] steps: - name: Checkout Code uses: actions/checkout@v3 - name: Cache NuGet packages uses: actions/cache@v3 with: path: ~/.nuget/packages key: ${{ runner.os }}-nuget-${{ hashFiles(\'**/*.csproj\') }} restore-keys: ${{ runner.os }}-nuget- - name: Setup .NET SDK uses: actions/setup-dotnet@v3 with: dotnet-version: ${{ matrix.dotnet-version }} cache: true - name: Restore Dependencies run: dotnet restore src/MyApp.sln --locked-mode - name: Build Solution run: dotnet build src/MyApp.sln --no-restore --configuration Release - name: Run Unit Tests run: dotnet test tests/MyApp.Tests/MyApp.Tests.csproj \\ --no-build --configuration Release --logger \"trx\" - name: Upload Test Results uses: actions/upload-artifact@v3 with: name: test-results path: \'**/*.trx\' retention-days: 7
🔄 子流程图
#mermaid-svg-dbFvfGreYP6A4NIU {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-dbFvfGreYP6A4NIU .error-icon{fill:#552222;}#mermaid-svg-dbFvfGreYP6A4NIU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dbFvfGreYP6A4NIU .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-dbFvfGreYP6A4NIU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dbFvfGreYP6A4NIU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dbFvfGreYP6A4NIU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dbFvfGreYP6A4NIU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dbFvfGreYP6A4NIU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dbFvfGreYP6A4NIU .marker.cross{stroke:#333333;}#mermaid-svg-dbFvfGreYP6A4NIU svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dbFvfGreYP6A4NIU .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dbFvfGreYP6A4NIU .cluster-label text{fill:#333;}#mermaid-svg-dbFvfGreYP6A4NIU .cluster-label span{color:#333;}#mermaid-svg-dbFvfGreYP6A4NIU .label text,#mermaid-svg-dbFvfGreYP6A4NIU span{fill:#333;color:#333;}#mermaid-svg-dbFvfGreYP6A4NIU .node rect,#mermaid-svg-dbFvfGreYP6A4NIU .node circle,#mermaid-svg-dbFvfGreYP6A4NIU .node ellipse,#mermaid-svg-dbFvfGreYP6A4NIU .node polygon,#mermaid-svg-dbFvfGreYP6A4NIU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dbFvfGreYP6A4NIU .node .label{text-align:center;}#mermaid-svg-dbFvfGreYP6A4NIU .node.clickable{cursor:pointer;}#mermaid-svg-dbFvfGreYP6A4NIU .arrowheadPath{fill:#333333;}#mermaid-svg-dbFvfGreYP6A4NIU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dbFvfGreYP6A4NIU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dbFvfGreYP6A4NIU .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-dbFvfGreYP6A4NIU .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-dbFvfGreYP6A4NIU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dbFvfGreYP6A4NIU .cluster text{fill:#333;}#mermaid-svg-dbFvfGreYP6A4NIU .cluster span{color:#333;}#mermaid-svg-dbFvfGreYP6A4NIU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dbFvfGreYP6A4NIU :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} ✅ Checkout 🗄️ Cache NuGet 📦 Setup SDK 🔄 Restore 🏗️ Build 🧪 Test 💾 Upload Artifacts
3️⃣ 🕵️ Static Analysis(SonarCloud & CodeQL)
📝 本 Job 目标:Shift‐Left 质量与安全保障
static-scan: name: 🕵️ Static Analysis runs-on: ubuntu-latest needs: build-test strategy: matrix: tool: [\'sonarcloud\',\'codeql\'] steps: - name: Checkout Full History uses: actions/checkout@v3 with: fetch-depth: 0 # SonarCloud - if: matrix.tool == \'sonarcloud\' name: SonarCloud Prepare uses: SonarSource/sonarcloud-github-action@v1.9.0 - if: matrix.tool == \'sonarcloud\' name: Build for SonarCloud run: dotnet build src/MyApp.sln --configuration Release - if: matrix.tool == \'sonarcloud\' name: SonarCloud Publish uses: SonarSource/sonarcloud-github-action@v1.9.0 # CodeQL - if: matrix.tool == \'codeql\' name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: csharp config-file: .github/codeql/codeql-config.yml - if: matrix.tool == \'codeql\' name: Autobuild uses: github/codeql-action/autobuild@v2 - if: matrix.tool == \'codeql\' name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2
🔄 子流程图
#mermaid-svg-0Q1V4wR6rKCHF7sl {font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-0Q1V4wR6rKCHF7sl .error-icon{fill:#552222;}#mermaid-svg-0Q1V4wR6rKCHF7sl .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0Q1V4wR6rKCHF7sl .edge-thickness-normal{stroke-width:2px;}#mermaid-svg-0Q1V4wR6rKCHF7sl .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0Q1V4wR6rKCHF7sl .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0Q1V4wR6rKCHF7sl .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0Q1V4wR6rKCHF7sl .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0Q1V4wR6rKCHF7sl .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0Q1V4wR6rKCHF7sl .marker.cross{stroke:#333333;}#mermaid-svg-0Q1V4wR6rKCHF7sl svg{font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0Q1V4wR6rKCHF7sl .label{font-family:\"trebuchet ms\",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0Q1V4wR6rKCHF7sl .cluster-label text{fill:#333;}#mermaid-svg-0Q1V4wR6rKCHF7sl .cluster-label span{color:#333;}#mermaid-svg-0Q1V4wR6rKCHF7sl .label text,#mermaid-svg-0Q1V4wR6rKCHF7sl span{fill:#333;color:#333;}#mermaid-svg-0Q1V4wR6rKCHF7sl .node rect,#mermaid-svg-0Q1V4wR6rKCHF7sl .node circle,#mermaid-svg-0Q1V4wR6rKCHF7sl .node ellipse,#mermaid-svg-0Q1V4wR6rKCHF7sl .node polygon,#mermaid-svg-0Q1V4wR6rKCHF7sl .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0Q1V4wR6rKCHF7sl .node .label{text-align:center;}#mermaid-svg-0Q1V4wR6rKCHF7sl .node.clickable{cursor:pointer;}#mermaid-svg-0Q1V4wR6rKCHF7sl .arrowheadPath{fill:#333333;}#mermaid-svg-0Q1V4wR6rKCHF7sl .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0Q1V4wR6rKCHF7sl .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0Q1V4wR6rKCHF7sl .edgeLabel{background-color:#e8e8e8;text-align:center;}#mermaid-svg-0Q1V4wR6rKCHF7sl .edgeLabel rect{opacity:0.5;background-color:#e8e8e8;fill:#e8e8e8;}#mermaid-svg-0Q1V4wR6rKCHF7sl .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0Q1V4wR6rKCHF7sl .cluster text{fill:#333;}#mermaid-svg-0Q1V4wR6rKCHF7sl .cluster span{color:#333;}#mermaid-svg-0Q1V4wR6rKCHF7sl div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\"trebuchet ms\",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0Q1V4wR6rKCHF7sl :root{--mermaid-font-family:\"trebuchet ms\",verdana,arial,sans-serif;} SonarCloud CodeQL 👥 Checkout 🔍 Tool? 📊 Run SonarCloud ⚙️ Init CodeQL 🚧 Autobuild 🔎 Analyze
4️⃣ 📦 Package & Publish(NuGet 与 Docker)
📝 本 Job 目标:仅在 Push 时执行包与镜像发布
package-publish: name: 📦 Package & Publish if: github.event_name == \'push\' && (github.ref == \'refs/heads/main\' || github.ref == \'refs/heads/develop\') runs-on: ubuntu-latest needs: static-scan steps: - name: Checkout Code uses: actions/checkout@v3 # NuGet - name: Pack NuGet Package run: dotnet pack src/MyApp.Application/MyApp.Application.csproj \\ --no-build -o ./artifacts - uses: NuGet/setup-nuget@v2 - name: Push to NuGet.org run: dotnet nuget push ./artifacts/*.nupkg \\ --api-key ${{ secrets.NUGET_API_KEY }} \\ --source https://api.nuget.org/v3/index.json # Docker (GHCR) - name: Login to GHCR uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build & Push Docker Image uses: docker/build-push-action@v4 with: context: src/MyApp.HttpApi.Host file: src/MyApp.HttpApi.Host/Dockerfile push: true tags: | ghcr.io/${{ github.repository_owner }}/myapp:${{ github.sha }} ghcr.io/${{ github.repository_owner }}/myapp:latest cache-from: type=gha,scope=src-MyApp.HttpApi.Host cache-to: type=gha,mode=max,scope=src-MyApp.HttpApi.Host
5️⃣ 🚀 Deploy to Staging(预发布环境)
📝 本 Job 目标:在 develop
分支推送时执行,需审批
deploy-staging: name: 🚀 Deploy to Staging runs-on: ubuntu-latest needs: package-publish if: github.ref == \'refs/heads/develop\' environment: staging steps: - name: Azure Login uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: Set AKS Context uses: azure/aks-set-context@v2 with: creds: ${{ secrets.AZURE_CREDENTIALS }} cluster-name: myCluster resource-group: myRG - name: Install EF CLI run: | dotnet tool install --global dotnet-ef --version 8.* echo \"$HOME/.dotnet/tools\" >> $GITHUB_PATH - name: Run EF Core Migrations run: dotnet ef database update \\ --project src/MyApp.EntityFrameworkCore/MyApp.EntityFrameworkCore.csproj \\ --startup-project src/MyApp.HttpApi.Host/MyApp.HttpApi.Host.csproj \\ --configuration Release - name: Helm Upgrade (Staging) run: | helm upgrade myapp-staging ./charts/myapp \\ --namespace staging --install \\ --set image.tag=${{ github.sha }}
6️⃣ 🏭 Deploy to Production(生产环境)
📝 本 Job 目标:在 main
分支推送时执行,需审批
deploy-prod: name: 🏭 Deploy to Production runs-on: ubuntu-latest needs: deploy-staging if: github.ref == \'refs/heads/main\' environment: production steps: - name: Azure Login uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: Set AKS Context uses: azure/aks-set-context@v2 with: creds: ${{ secrets.AZURE_CREDENTIALS }} cluster-name: myCluster resource-group: myRG - name: Helm Upgrade (Production) run: | helm upgrade myapp-prod ./charts/myapp \\ --namespace prod --install \\ --set image.tag=${{ github.sha }}
7️⃣ ⏪ Rollback & Alert(自动回滚与告警)
Rollback Staging
rollback-staging: name: ⏪ Rollback & Notify (Staging) if: failure() && github.ref == \'refs/heads/develop\' runs-on: ubuntu-latest needs: deploy-staging steps: - name: Determine Last Successful Revision id: hist run: | rev=$(helm history myapp-staging -n staging --max 5 \\ --output json | jq -r \'.[] | select(.status==\"DEPLOYED\") | .revision\' | tail -1) if [[ -z \"$rev\" || \"$rev\" -le 0 ]]; then echo \"No valid revision to rollback\"; exit 1 fi echo \"::set-output name=revision::$rev\" - name: Helm Rollback (Staging) run: helm rollback myapp-staging ${{ steps.hist.outputs.revision }} -n staging - name: Slack Notification uses: rtCamp/action-slack-notify@v2 with: webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} message: \":warning: Staging 部署失败,已回滚到 revision ${{ steps.hist.outputs.revision }}\"
Rollback Production
rollback-prod: name: ⏪ Rollback & Notify (Production) if: failure() && github.ref == \'refs/heads/main\' runs-on: ubuntu-latest needs: deploy-prod steps: - name: Determine Last Successful Revision id: hist run: | rev=$(helm history myapp-prod -n prod --max 5 \\ --output json | jq -r \'.[] | select(.status==\"DEPLOYED\") | .revision\' | tail -1) if [[ -z \"$rev\" || \"$rev\" -le 0 ]]; then echo \"No valid revision to rollback\"; exit 1 fi echo \"::set-output name=revision::$rev\" - name: Helm Rollback (Production) run: helm rollback myapp-prod ${{ steps.hist.outputs.revision }} -n prod - name: Slack Notification uses: rtCamp/action-slack-notify@v2 with: webhook_url: ${{ secrets.SLACK_WEBHOOK_URL }} message: \":x: Production 部署失败,已回滚到 revision ${{ steps.hist.outputs.revision }}\"
🔧 ABP VNext 专属集成示例
Program.cs 示例
var builder = WebApplication.CreateBuilder(args);builder.Services.AddApplication<MyAppHttpApiHostModule>();builder.Host.UseAutofac();var app = builder.Build();app.UseAbpRequestLocalization();app.UseAbpSwaggerUI(options =>{ options.SwaggerEndpoint(\"/swagger/v1/swagger.json\", \"MyApp API V1\");});app.UseRouting();app.UseAuthentication();app.UseAuthorization();app.MapControllers();app.MapHealthChecks(\"/health\");app.MapHealthChecks(\"/health/ready\", new HealthCheckOptions{ Predicate = reg => reg.Name.Contains(\"ready\")});app.Run();
Helm Chart 探针示例 (charts/myapp/values.yaml
)
livenessProbe: httpGet: path: /health port: http initialDelaySeconds: 30readinessProbe: httpGet: path: /health/ready port: http initialDelaySeconds: 10
📦 附录:配置文件
sonar-project.properties
sonar.projectKey=sonar.organization=sonar.sources=srcsonar.tests=testssonar.dotnet.visualstudio.solution.file=src/MyApp.sln
CodeQL 配置 (.github/codeql/codeql-config.yml
)
queries: - security-and-quality - security-and-performance