ci-cd

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

CI/CD

CI/CD

Core Principles

核心原则

  1. Pipeline as code — YAML pipelines committed to the repo. No click-ops in the UI.
  2. Fast feedback — Build and test on every push. Cache NuGet packages. Fail fast.
  3. Build once, deploy many — Build the artifact once, promote it through environments (dev → staging → production).
  4. Never skip tests — Tests gate the pipeline. No deployment without passing tests.
  1. 流水线即代码 — 将YAML流水线提交到代码仓库。不在UI中进行点击式操作。
  2. 快速反馈 — 每次推送代码时执行构建和测试。缓存NuGet包。快速失败。
  3. 一次构建,多次部署 — 仅构建一次工件,然后在各个环境中推广部署(开发 → 预发布 → 生产)。
  4. 绝不跳过测试 — 测试是流水线的关卡。未通过测试则无法部署。

Patterns

模式

GitHub Actions — Build + Test

GitHub Actions — 构建 + 测试

yaml
undefined
yaml
undefined

.github/workflows/ci.yml

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

GitHub Actions — Build + Publish Docker Image

GitHub Actions — 构建 + 发布Docker镜像

yaml
undefined
yaml
undefined

.github/workflows/publish.yml

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

Azure DevOps — Build + Test

Azure DevOps — 构建 + 测试

Same restore → build → format → test flow as GitHub Actions. Key differences:
yaml
undefined
流程与GitHub Actions一致:还原 → 构建 → 格式检查 → 测试。主要差异:
yaml
undefined

azure-pipelines.yml

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'
trigger: branches: include: [main] paths: exclude: ['*.md', docs/]
pool: vmImage: 'ubuntu-latest' # 对应GitHub Actions的runs-on: ubuntu-latest
variables: dotnetVersion: '10.0.x'

Key task differences from GitHub Actions:

与GitHub Actions的主要任务差异:

Setup .NET: task: UseDotNet@2 (inputs: version: $(dotnetVersion))

Setup .NET: task: UseDotNet@2 (inputs: version: $(dotnetVersion))

Test results: task: PublishTestResults@2 (testResultsFormat: VSTest)

测试结果: task: PublishTestResults@2 (testResultsFormat: VSTest)

Steps use
script:
+
displayName:
instead of
- name:
+
run:

步骤使用
script:
+
displayName:
替代
- name:
+
run:

Services (e.g., Postgres) require a separate Docker task or pipeline service connection

服务(如Postgres)需要单独的Docker任务或流水线服务连接

undefined
undefined

NuGet Package Publishing

NuGet包发布

yaml
undefined
yaml
undefined

Part of GitHub Actions workflow

GitHub Actions工作流的一部分

  • 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
undefined
  • 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
undefined

Anti-patterns

反模式

Don't Build Different Artifacts per Environment

不要为每个环境构建不同的工件

yaml
undefined
yaml
undefined

BAD — building separately for each environment

错误示例 — 为每个环境单独构建

  • script: dotnet publish -c Debug # for dev
  • script: dotnet publish -c Release # for prod
  • script: dotnet publish -c Debug # 开发环境
  • script: dotnet publish -c Release # 生产环境

GOOD — build once, deploy everywhere

正确示例 — 一次构建,全环境部署

  • script: dotnet publish -c Release -o ./publish
  • script: dotnet publish -c Release -o ./publish

Then deploy the same ./publish artifact to dev, staging, prod

然后将同一个./publish工件部署到开发、预发布、生产环境

undefined
undefined

Don't Skip Format Checks in CI

不要在CI中跳过格式检查

yaml
undefined
yaml
undefined

BAD — no format enforcement

错误示例 — 没有格式校验

steps:
  • run: dotnet build
  • run: dotnet test
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
undefined
steps:
  • run: dotnet build
  • run: dotnet format --verify-no-changes
  • run: dotnet test
undefined

Don't Hardcode Secrets in Pipelines

不要在流水线中硬编码密钥

yaml
undefined
yaml
undefined

BAD — secret in pipeline YAML

错误示例 — 密钥写在流水线YAML中

env: DB_PASSWORD: "my-secret-password"
env: DB_PASSWORD: "my-secret-password"

GOOD — use pipeline secrets

正确示例 — 使用流水线密钥

env: DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
undefined
env: DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
undefined

Decision Guide

决策指南

ScenarioRecommendation
Open source projectGitHub Actions
Enterprise with AzureAzure DevOps Pipelines
Docker deploymentMulti-stage build in CI, push to container registry
NuGet libraryBuild → Test → Pack → Push on tag
Database migrationsRun in CI test stage, script for production
Environment promotionSame artifact, different configuration
场景推荐方案
开源项目GitHub Actions
使用Azure的企业Azure DevOps Pipelines
Docker部署CI中使用多阶段构建,推送到容器注册表
NuGet类库打标签时执行构建 → 测试 → 打包 → 推送
数据库迁移在CI测试阶段运行,生产环境使用脚本执行
环境推广使用同一工件,不同配置