mcpField
D
dotnet-gha-deploy

dotnet-gha-deploy

github.com/wshaddix云与 DevOps8671 次安装35 周前更新
代码仓库

关于 dotnet-gha-deploy

为 .NET 项目提供 GitHub Actions 部署模式,面向维护文档站点、容器和 Azure Web Apps 部署流水线的开发者。内容涵盖环境审批、OIDC 身份验证和回滚。

主要功能

  1. 01GitHub Pages 文档部署
  2. 02GHCR 与 ACR 镜像推送
  3. 03Azure Web Apps 部署
  4. 04GitHub Environments 部署门禁
  5. 05部署回滚策略

适用场景

  1. 01将项目文档发布到 GitHub Pages
  2. 02按环境推广相同容器镜像
  3. 03通过部署槽发布 Azure Web Apps
  4. 04部署失败后恢复先前版本

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-gha-deploy

安装后在 Claude Code 中输入/dotnet-gha-deploy确认已加载。

详细介绍

dotnet-gha-deploy

GitHub Actions 中 .NET 应用程序的部署模式:文档站点(Starlight/Docusaurus)的 GitHub Pages 部署、GHCR 和 ACR 的容器注册表推送模式、通过 azure/webapps-deploy 部署到 Azure Web Apps、使用保护规则实现分阶段发布的 GitHub Environments,以及部署失败后的回滚策略。

版本假设: GitHub Actions 工作流语法 v2。Azure App Service 使用 azure/webapps-deploy@v3。使用 azure/login@v2 管理 Azure 凭据。使用 GitHub Environments 设置部署门禁。

范围边界: 此技能负责 GitHub Actions 部署流水线模式。容器编排和运行时配置由 [skill:dotnet-container-deployment] 负责。容器映像编写由 [skill:dotnet-containers] 负责。发布(NuGet 推送、容器构建)归属 [skill:dotnet-gha-publish]。可组合工作流模式见 [skill:dotnet-gha-patterns]。入门 CI 模板由 [skill:dotnet-add-ci] 负责。

范围之外: 容器编排(Kubernetes、Docker Compose)——参见 [skill:dotnet-container-deployment]。容器映像编写——参见 [skill:dotnet-containers]。NuGet 发布和容器构建——参见 [skill:dotnet-gha-publish]。入门 CI 模板——参见 [skill:dotnet-add-ci]。Azure DevOps 部署——参见 [skill:dotnet-ado-patterns]。CLI 发布流水线——参见 [skill:dotnet-cli-release-pipeline]。

交叉引用:[skill:dotnet-container-deployment](容器编排模式)、[skill:dotnet-containers](容器映像编写)、[skill:dotnet-add-ci](入门 CI 模板)、[skill:dotnet-cli-release-pipeline](CLI 专用发布自动化)。


GitHub Pages 文档部署

静态站点部署(Starlight/Docusaurus)

将 .NET 项目的文档站点部署到 GitHub Pages:

name: Deploy Docs

on:
  push:
    branches: [main]
    paths:
      - 'docs/**'
      - '.github/workflows/deploy-docs.yml'
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: false

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

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: npm
          cache-dependency-path: docs/package-lock.json

      - name: Install dependencies
        working-directory: docs
        run: npm ci

      - name: Build documentation site
        working-directory: docs
        run: npm run build

      - name: Upload Pages artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: docs/dist

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

关键决策:

  • concurrency.cancel-in-progress: false 可防止取消正在进行的 Pages 部署
  • Pages 部署令牌需要 id-token: write 权限
  • 将 build 和 deploy 作业分开,可让 deploy 作业使用带有保护规则的 github-pages 环境
从 XML 注释生成 API 文档

根据 .NET XML 注释生成并部署 API 参考文档:

- name: Setup .NET
  uses: actions/setup-dotnet@v4
  with:
    dotnet-version: '8.0.x'

- name: Build with XML docs
  run: |
    set -euo pipefail
    dotnet build src/MyLibrary/MyLibrary.csproj \
      -c Release \
      -p:GenerateDocumentationFile=true

- name: Generate API docs with docfx
  run: |
    set -euo pipefail
    dotnet tool install -g docfx
    docfx docs/docfx.json

- name: Upload Pages artifact
  uses: actions/upload-pages-artifact@v3
  with:
    path: docs/_site

容器注册表推送模式

通过环境门禁推送到 GHCR
jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      image-digest: ${{ steps.build.outputs.digest }}
    steps:
      - uses: actions/checkout@v4

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}

  deploy-staging:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: https://staging.example.com
    steps:
      - name: Deploy container to staging
        run: |
          set -euo pipefail
          echo "Deploying ghcr.io/${{ github.repository }}@${{ needs.build.outputs.image-digest }} to staging"
          # Platform-specific deployment command here

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
    steps:
      - name: Deploy container to production
        run: |
          set -euo pipefail
          echo "Deploying ghcr.io/${{ github.repository }}@${{ needs.build.outputs.image-digest }} to production"
按摘要提升(不可变部署)

使用映像摘要引用,在各环境之间进行不可变部署:

- name: Retag for production
  run: |
    set -euo pipefail
    # Pull by digest (immutable), retag for production
    docker pull ghcr.io/${{ github.repository }}@${{ needs.build.outputs.image-digest }}
    docker tag ghcr.io/${{ github.repository }}@${{ needs.build.outputs.image-digest }} \
      ghcr.io/${{ github.repository }}:production
    docker push ghcr.io/${{ github.repository }}:production

基于摘要的提升可确保部署到生产环境的是完全相同的映像字节,不受标签变更影响。


Azure Web Apps 部署

通过 azure/webapps-deploy 部署
name: Deploy to Azure

on:
  push:
    branches: [main]

permissions:
  contents: read
  id-token: write

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

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'

      - name: Publish
        run: |
          set -euo pipefail
          dotnet publish src/MyApp/MyApp.csproj \
            -c Release \
            -o ./publish

      - name: Upload publish artifact
        uses: actions/upload-artifact@v4
        with:
          name: webapp
          path: ./publish

  deploy-staging:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: https://myapp-staging.azurewebsites.net
    steps:
      - name: Download artifact
        uses: actions/download-artifact@v4
        with:
          name: webapp
          path: ./publish

      - name: Login to Azure
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

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

  deploy-production:
    needs: 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: webapp
          path: ./publish

      - name: Login to Azure
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Deploy to Azure Web App
        uses: azure/webapps-deploy@v3
        with:
          app-name: myapp-production
          package: ./publish
带部署槽位的 Azure Web App

使用部署槽位实现零停机部署,并在交换前进行验证:

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

- name: Validate staging slot
  shell: bash
  run: |
    set -euo pipefail
    HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
      https://myapp-production-staging.azurewebsites.net/healthz)
    if [ "$HTTP_STATUS" != "200" ]; then
      echo "Health check failed with status $HTTP_STATUS"
      exit 1
    fi

- name: Swap slots
  uses: azure/cli@v2
  with:
    inlineScript: |
      az webapp deployment slot swap \
        --resource-group myapp-rg \
        --name myapp-production \
        --slot staging \
        --target-slot production
OIDC 身份验证(联合凭据)

使用 OIDC 进行无密码 Azure 身份验证,而不是使用服务主体密钥:

- name: Login to Azure (OIDC)
  uses: azure/login@v2
  with:
    client-id: ${{ secrets.AZURE_CLIENT_ID }}
    tenant-id: ${{ secrets.AZURE_TENANT_ID }}
    subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

OIDC 要求在 Azure AD 中配置联合凭据,以信任 GitHub Actions OIDC 提供方。GitHub Secrets 中不会存储客户端密钥。


带保护规则的 GitHub Environments

多环境流水线
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: dotnet publish -c Release -o ./publish
      - uses: actions/upload-artifact@v4
        with:
          name: app
          path: ./publish

  deploy-dev:
    needs: build
    runs-on: ubuntu-latest
    environment: development
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: app
      - run: echo "Deploy to dev"

  deploy-staging:
    needs: deploy-dev
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: https://staging.example.com
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: app
      - run: echo "Deploy to staging"

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: app
      - run: echo "Deploy to production"
保护规则配置

在每个环境的 GitHub Settings > Environments 中进行配置:

环境所需审核者等待时间分支策略
development无无任意分支
staging1 位审核者无main、release/*
production2 位审核者15 分钟仅限 main
特定环境的密钥和变量

每个环境都可以覆盖仓库级别的密钥:

jobs:
  deploy:
    environment: production
    runs-on: ubuntu-latest
    steps:
      - name: Deploy with environment-specific config
        env:
          # Resolves to the production environment's secret, not the repo-level one
          DB_CONNECTION: ${{ secrets.DB_CONNECTION_STRING }}
          APP_URL: ${{ vars.APP_URL }}
        run: |
          set -euo pipefail
          echo "Deploying to $APP_URL"

回滚模式

回退部署

发生故障时,重新部署上一个已知正常的版本:

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy new version
        id: deploy
        continue-on-error: true
        run: |
          set -euo pipefail
          # Deploy logic here
          ./deploy.sh --version ${{ github.sha }}

      - name: Health check
        id: health
        if: steps.deploy.outcome == 'success'
        continue-on-error: true
        shell: bash
        run: |
          set -euo pipefail
          for i in {1..5}; do
            HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://example.com/healthz)
            if [ "$HTTP_STATUS" = "200" ]; then
              echo "Health check passed"
              exit 0
            fi
            sleep 10
          done
          echo "Health check failed after 5 attempts"
          exit 1

      - name: Rollback on failure
        if: steps.deploy.outcome == 'failure' || steps.health.outcome == 'failure'
        run: |
          set -euo pipefail
          echo "Rolling back to previous version"
          # Re-deploy the last known-good artifact
          ./deploy.sh --version ${{ github.event.before }}

      - name: Fail the job if rollback was needed
        if: steps.deploy.outcome == 'failure' || steps.health.outcome == 'failure'
        run: exit 1
Azure 部署槽位回滚

健康检查失败时,切换回之前的槽位:

- name: Swap to production
  id: swap
  uses: azure/cli@v2
  with:
    inlineScript: |
      az webapp deployment slot swap \
        --resource-group myapp-rg \
        --name myapp-production \
        --slot staging \
        --target-slot production

- name: Post-swap health check
  id: post-health
  continue-on-error: true
  shell: bash
  run: |
    set -euo pipefail
    sleep 30  # allow swap to stabilize
    HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://myapp.azurewebsites.net/healthz)
    if [ "$HTTP_STATUS" != "200" ]; then
      echo "Post-swap health check failed"
      exit 1
    fi

- name: Rollback swap on failure
  if: steps.post-health.outcome == 'failure'
  uses: azure/cli@v2
  with:
    inlineScript: |
      az webapp deployment slot swap \
        --resource-group myapp-rg \
        --name myapp-production \
        --slot staging \
        --target-slot production
      echo "Rolled back: swapped staging back to production"
通过 workflow_dispatch 手动回滚

提供手动触发方式,以便在紧急情况下回滚:

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Version to roll back to (e.g., v1.2.3)'
        required: true
        type: string
      environment:
        description: 'Target environment'
        required: true
        type: choice
        options:
          - staging
          - production

jobs:
  rollback:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.version }}

      - name: Publish
        run: |
          set -euo pipefail
          dotnet publish src/MyApp/MyApp.csproj -c Release -o ./publish

      - name: Deploy rollback version
        run: |
          set -euo pipefail
          echo "Rolling back ${{ inputs.environment }} to ${{ inputs.version }}"
          # Platform-specific deployment

Agent 注意事项

  1. 在所有多行 bash 步骤中使用 set -euo pipefail —— 不使用 pipefail 时,管道命令中的失败会被悄悄忽略,导致部署看似成功。
  2. 部署并发组中绝不要使用 cancel-in-progress: true —— 取消正在进行的部署可能导致基础设施处于部分部署状态。
  3. 部署后务必运行运行状况检查 —— deploy 步骤成功并不代表应用运行正常;请通过 HTTP 运行状况检查进行验证。
  4. 使用 id-token: write 权限进行 OIDC Azure 登录 —— 缺少此权限时,联合凭据交换会失败,并返回含义不明的 403 错误。
  5. 部署槽位交换是原子操作 —— 如果交换失败,两个槽位都会保留原有部署,不会出现部分状态。
  6. 绝不要在工作流文件中硬编码 Azure 凭据 —— 请使用 OIDC 联合凭据或限定环境的机密;硬编码在 YAML 中的机密会在仓库历史记录中公开。
  7. 生产部署使用基于摘要的镜像引用 —— 标签可以修改和覆盖;摘要不可变,能够保证镜像字节完全一致。
  8. 将构建和部署拆分为不同的作业 —— 只构建一次构建产物,再从同一构建产物部署到多个环境,以确保一致性。

评论

支持 Markdown